Here I illustrate the use of the Memento Pattern to restore certain (but not all) data of a class that has data in abstract base classes. A Spellcaster
has spells and is derived from an abstract CharacterClass
that has levels, etc... data,
which in turn is derived from LivingBeing
which has hitPoints
, etc... data, and all of these are to be restored to their previous values in previous levels. But name
and some other data in LivingBeing
shall not be restored, nor should
wandsPossessed
in Spellcaster
. A vector of LevelMemento
s is stored throughout the hierarchy to restore the data in each of these classes. The data that is to restored is stored in another inner class called Data
, and so
the inner class LevelMemento
uses the inner class Data
in its constructor. All other data that is not to be restored are placed outside the inner class Data
. The methods save(std::ostream&)
and load(std::istream&)
are not needed in my code, but I wanted to test them out and illustrate that the Data
inner classes
also help to implemente them without needing to rewrite any duplicate lines when the LevelMemento
s are written and read from a text file.
#include <iostream>
#include <memory>
#include <vector>
#include <list>
#include <array>
#include <fstream>
enum Ability { Strength, Intelligence, Wisdom, Dexterity, Constitution, Charisma, NumAbilityScores, FirstAbilityIndex = Strength, LastAbilityIndex = Charisma };
std::list<class LivingBeing*> allBeingsPresent;
class LivingBeing {
struct Data { // Data stores all the LivingBeing data that shall be restored to what they were in previous levels after energy drains.
int hitPoints, numAttacks = 1;
void save (std::ostream& os) const {
os << hitPoints << ' ' << numAttacks << '\n';
}
void load (std::istream& is) {
is >> std::skipws >> hitPoints >> numAttacks;
}
};
struct LevelMemento {
Data data;
LevelMemento (const Data& d) : data(d) { }
LevelMemento() = default;
void save (std::ostream& os) const { data.save(os); } // Thanks to the Data struct, this needs not be rewritten.
void load (std::istream& is) { data.load(is); } // Thanks to the Data struct, this needs not be rewritten.
};
Data data;
std::string name; // name will NOT be restored to what it was in previous levels after energy drains, nor will AC, encumbrance, etc... below.
int AC = 9, encumbrance = 800;
std::vector<std::unique_ptr<LevelMemento>> levelMementos;
public:
LivingBeing (const std::string& n, int hp) : name(n) { data.hitPoints = hp; allBeingsPresent.push_back(this); }
LivingBeing() = default;
std::string getName() const { return name; }
void changeName (const std::string& newName) { name = newName; }
virtual void levelUp() {
std::cout << '\n' << name << " has levelled up.\n";
levelMementos.push_back(std::unique_ptr<LevelMemento>(new LevelMemento(data))); // Add a snapshot of the current value of 'data' to levelMementos.
data.hitPoints += 5;
}
virtual void revertToPreviousLevel (int previousLevel) {
data = levelMementos[previousLevel - 1]->data;
levelMementos.resize(previousLevel); // Remove all elements of levelMementos after index previousLevel - 1.
}
virtual void receiveEnergyDrain (int numLevels = 1) = 0; // Not only does CharacterClass override this, but so does Monster (its hitDice will represent its level).
virtual std::string saveName() const = 0; // LivingBeing is abstract, but LivingBeing::data will still be restored via levelMementos.
virtual void save (std::ostream& os) const {
os << saveName() << '\n';
data.save(os);
os << name << '\n';
os << AC << ' ' << encumbrance << '\n';
os << levelMementos.size() << '\n';
for (const std::unique_ptr<LevelMemento>& memento : levelMementos)
memento->save(os);
}
virtual void load (std::istream& is) {
std::string name;
int numMementos;
data.load(is);
while (std::getline(is, name) && name.empty());
is >> std::skipws >> AC >> encumbrance;
is >> numMementos;
for (int i = 0; i < numMementos; ++i) {
std::unique_ptr<LevelMemento> memento = std::make_unique<LevelMemento>();
memento->load(is);
levelMementos.push_back(std::move(memento));
}
}
virtual void display() const {
std::cout << "Name: " << name << '\n';
std::cout << "Hit points: " << data.hitPoints << '\n';
}
};
class CharacterClass : public LivingBeing {
struct Data { // Data stores all the CharacterClass data that shall be restored to what they were in previous levels after energy drains. CharacterClass is abstract, but CharacterClass::data will still be restored via CharacterClass::levelMementos.
int level = 1, experiencePoints = 0;
std::array<int, NumAbilityScores> abilities = {{10,14,10,15,12,9}};
void save (std::ostream& os) const {
os << level << ' ' << experiencePoints << '\n';
for (int n : abilities)
os << ' ' << n;
os << '\n';
}
void load (std::istream& is) {
is >> std::skipws >> level >> experiencePoints;
for (int i = 0; i < NumAbilityScores; ++i)
is >> std::skipws >> abilities[i-1];
}
};
struct LevelMemento {
Data data;
LevelMemento (const Data& d) : data(d) { }
LevelMemento() = default;
void save (std::ostream& os) const { data.save(os); } // Thanks to the Data struct, this needs not be rewritten.
void load (std::istream& is) { data.load(is); } // Thanks to the Data struct, this needs not be rewritten.
};
Data data;
std::vector<std::unique_ptr<LevelMemento>> levelMementos;
public:
using LivingBeing::LivingBeing;
int getAbilityScore (Ability a) const { return data.abilities[a]; }
void setAbilityScore (Ability a, int value) { data.abilities[a] = value; }
virtual void levelUp() {
levelMementos.push_back(std::unique_ptr<LevelMemento>(new LevelMemento(data))); // Add a snapshot of the current value of CharacterClass::data to CharacterClass::levelMementos.
LivingBeing::levelUp(); // Add a snapshot of LivingBeing::data to LivingBeing::levelMementos.
data.level++;
data.experiencePoints += 5000;
}
virtual void revertToPreviousLevel (int previousLevel) override {
LivingBeing::revertToPreviousLevel(previousLevel); // Restores the data in LivingBeing that is not in CharacterClass (i.e. hitPoints, numAttacks, etc...).
data = levelMementos[previousLevel - 1]->data;
levelMementos.resize(previousLevel); // Remove all elements of levelMementos after index previousLevel - 1.
}
virtual void receiveEnergyDrain (int numLevels = 1) override {
std::cout << '\n' << getName() << " has been energy drained. Number of levels lost: " << numLevels << '\n';
revertToPreviousLevel(data.level - numLevels);
}
virtual void save (std::ostream& os) const override {
LivingBeing::save(os);
saveParticular(os);
}
virtual void load (std::istream& is) override {
LivingBeing::load(is);
loadParticular(is);
}
virtual void display() const {
LivingBeing::display();
for (int a : data.abilities)
std::cout << a << ' ';
std::cout << "\nLevel: " << data.level << '\n';
std::cout << "ExperiencePoints: " << data.experiencePoints << '\n';
}
private:
void saveParticular (std::ostream& os) const {
data.save(os);
os << levelMementos.size() << '\n';
for (const std::unique_ptr<LevelMemento>& memento : levelMementos)
memento->save(os);
}
void loadParticular (std::istream& is) {
int numMementos;
data.load(is);
is >> numMementos;
for (int i = 0; i < numMementos; ++i) {
std::unique_ptr<LevelMemento> memento = std::make_unique<LevelMemento>();
memento->load(is);
levelMementos.push_back(std::move(memento));
}
}
};
struct Spell {
void save (std::ostream&) const { } // Implementation omitted.
void load (std::istream&) { } // Implementation omitted.
};
struct Wand {
void save (std::ostream&) const { } // Implementation omitted.
void load (std::istream&) { } // Implementation omitted.
};
class Spellcaster : public CharacterClass {
struct Data { // Spellcaster::Data stores all the Spellcaster data that shall be restored to what they were in previous levels after energy drains.
std::list<std::shared_ptr<Spell>> fullListOfSpells;
void save (std::ostream& os) const {
os << fullListOfSpells.size() << '\n';
for (const std::shared_ptr<Spell>& spell : fullListOfSpells)
spell->save(os);
}
void load (std::istream& is) {
int numSpells;
is >> numSpells;
for (int i = 0; i < numSpells; ++i) {
std::shared_ptr<Spell> spell = std::make_shared<Spell>();
spell->load(is);
fullListOfSpells.push_back(spell);
}
}
};
struct LevelMemento {
Data data;
LevelMemento (const Data& d) : data(d) { }
LevelMemento() = default;
void save (std::ostream& os) const { data.save(os); } // Thanks to the Data struct, this needs not be rewritten.
void load (std::istream& is) { data.load(is); } // Thanks to the Data struct, this needs not be rewritten.
};
Data data;
std::list<std::shared_ptr<Spell>> currentListOfSpells; // currentListOfSpells will NOT be restored to what it was in previous levels after energy drains, nor will wandsPossessed, etc... below.
std::list<Wand*> wandsPossessed;
std::vector<std::unique_ptr<LevelMemento>> levelMementos;
static const std::string tag;
friend class System;
public:
Spellcaster (const std::string& name, int hitPoints, const std::list<std::shared_ptr<Spell>>& spells) : CharacterClass(name, hitPoints), data({spells}), currentListOfSpells(spells) { }
Spellcaster() = default;
void addWand (Wand* wand) { wandsPossessed.push_back(wand); }
virtual void revertToPreviousLevel (int previousLevel) override {
CharacterClass::revertToPreviousLevel(previousLevel); // Restores the data in CharacterClass that is not in Spellcaster (i.e. abilities, level, experiencePoints, etc...).
data = levelMementos[previousLevel - 1]->data; // Restores the data in Spellcaster specifically (i.e. fullListOfSpells).
levelMementos.resize(previousLevel); // Remove all elements of levelMementos after index previousLevel - 1.
}
virtual void levelUp() override {
levelMementos.push_back(std::unique_ptr<LevelMemento>(new LevelMemento(data))); // Add a snapshot of the current value of Spellcaster::data to Spellcaster::levelMementos.
CharacterClass::levelUp(); // Add a snapshot of CharacterClass::data to LivingBeing::levelMementos.
std::shared_ptr<Spell> spell = std::make_shared<Spell>();
data.fullListOfSpells.push_back(spell);
currentListOfSpells.push_back(spell);
};
virtual void save (std::ostream& os) const override {
CharacterClass::save(os);
saveParticular(os);
}
virtual void load (std::istream& is) override {
CharacterClass::load(is);
loadParticular(is);
}
virtual void display() const override {
CharacterClass::display();
std::cout << "Full list of spells known: " << data.fullListOfSpells.size() << '\n';
std::cout << "Number of wands: " << wandsPossessed.size() << '\n';
}
private:
virtual std::string saveName() const override { return tag; }
void saveParticular (std::ostream& os) const {
data.save(os);
os << currentListOfSpells.size() << '\n';
for (const std::shared_ptr<Spell>& spell : currentListOfSpells)
spell->save(os);
os << wandsPossessed.size() << '\n';
for (const Wand* wand : wandsPossessed)
wand->save(os);
os << levelMementos.size() << '\n';
for (const std::unique_ptr<LevelMemento>& memento : levelMementos)
memento->save(os);
}
void loadParticular (std::istream& is) {
data.load(is);
int num;
is >> num;
for (int i = 0; i < num; ++i) {
std::shared_ptr<Spell> spell = std::make_shared<Spell>();
spell->load(is);
currentListOfSpells.push_back(spell);
}
is >> num;
for (int i = 0; i < num; ++i) {
Wand* wand = new Wand;
wand->load(is);
wandsPossessed.push_back(wand);
}
is >> num;
for (int i = 0; i < num; ++i) {
std::unique_ptr<LevelMemento> memento = std::make_unique<LevelMemento>();
memento->load(is);
levelMementos.push_back(std::move(memento));
}
}
};
const std::string Spellcaster::tag = "Spellcaster";
struct System {
static const std::string saveFileName;
static void save();
static void load();
};
const std::string System::saveFileName = "SaveFile.txt";
void System::save() {
std::ofstream outfile(saveFileName);
outfile << allBeingsPresent.size() << '\n';
for (const LivingBeing* being : allBeingsPresent)
being->save(outfile);
}
void System::load() {
std::ifstream infile(saveFileName);
int numBeings;
std::string type;
infile >> numBeings;
for (int i = 0; i < numBeings; ++i) {
while (std::getline(infile, type) && type.empty());
if (type == Spellcaster::tag) {
Spellcaster* p = new Spellcaster;
p->load(infile);
allBeingsPresent.push_back(p);
}
// else if etc...
}
}
int main() {
Spellcaster* merlin = new Spellcaster("Merlin", 5, { std::make_shared<Spell>() } );
merlin->display();
merlin->levelUp();
merlin->display();
merlin->levelUp();
merlin->display();
merlin->changeName("Merlin the Magic-User");
merlin->addWand(new Wand); // Note that the wands possessed will NOT be restored to what they were during previous levels after energy drains.
merlin->setAbilityScore(Intelligence, 18);
std::cout << "\nMerlin has changed his name to " << merlin->getName() << " after he found a wand, and his intelligence changed to " << merlin->getAbilityScore(Intelligence) << ".\n";
// Note that his name will NOT be restored to what it was during previous levels after energy drains, but intelligence SHALL be restored.
merlin->levelUp();
merlin->display();
merlin->receiveEnergyDrain();
merlin->display();
merlin->receiveEnergyDrain(2);
merlin->display();
}
Output:
Name: Merlin
Hit points: 5
10 14 10 15 12 9
Level: 1
ExperiencePoints: 0
Full list of spells known: 1
Number of wands: 0
Merlin has levelled up.
Name: Merlin
Hit points: 10
10 14 10 15 12 9
Level: 2
ExperiencePoints: 5000
Full list of spells known: 2
Number of wands: 0
Merlin has levelled up.
Name: Merlin
Hit points: 15
10 14 10 15 12 9
Level: 3
ExperiencePoints: 10000
Full list of spells known: 3
Number of wands: 0
Merlin has changed his name to Merlin the Magic-User after he found a wand, and his intelligence changed to 18.
Merlin the Magic-User has levelled up.
Name: Merlin the Magic-User
Hit points: 20
10 18 10 15 12 9
Level: 4
ExperiencePoints: 15000
Full list of spells known: 4
Number of wands: 1
Merlin the Magic-User has been energy drained. Number of levels lost: 1
Name: Merlin the Magic-User
Hit points: 15
10 18 10 15 12 9
Level: 3
ExperiencePoints: 10000
Full list of spells known: 3
Number of wands: 1
Merlin the Magic-User has been energy drained. Number of levels lost: 2
Name: Merlin the Magic-User
Hit points: 5
10 14 10 15 12 9
Level: 1
ExperiencePoints: 0
Full list of spells known: 1
Number of wands: 1
Update: Since this posting, I've rewritten the code so that LevelMemento
has been defined outside the classes and now has a template parameter T for the class it is being used in. So now the LevelMemento<T>
class is written out only once. I'm looking for more ways to shorten the code.