A Multiclass is a CharacterClass
that decided to add new CharacterClass types to combine with the original CharacterClass. These new CharacterClass types are stored in a std::vector<CharacterClass*>
data member of CharacterClass called otherClasses
.
Common operations are used through iterating through originalClass
, and hence the Composite Pattern is being used. Furthermore, each of these multiclass component classes have a CharacterClass*
data member called originalClass
, which points to the
original CharacterClass. This pointer originalClass
is needed so that these muticlass components will forward to originalClass
whenever data of CharacterClass
is being used or changed, and thus the Proxy Pattern is being used, as they are in a sense
a proxy for originalClass
in terms of usage of these CharacterClass
data, but note that they do not act as proxies when using data and methods unique to themselves and not accessible from CharacterClass
.
#include <iostream>
#include <vector>
#include <map>
#include <functional>
#include <set>
#include <memory>
#include <algorithm>
enum CharacterClassType { FIGHTER, WIZARD, CLERIC, NUM_CHARACTER_TYPES };
static bool ask_yn (const std::string& question) {
while (true) {
std::cout << question << " (y/n) ";
char yesNo;
if (std::cin >> yesNo) {
switch (yesNo) {
case 'y': case 'Y': return true;
case 'n': case 'N': return false;
}
}
else { // Input failed. Assume no.
std::cout << '\n';
return false;
}
}
}
class LivingBeing {
std::string name;
int hitPoints;
std::vector<struct Weapon*> weaponsCarried;
public:
LivingBeing (const std::string& n) : name(n), hitPoints(10) { }
LivingBeing() = default;
virtual ~LivingBeing() = default;
virtual std::string getName() const { return name; }
virtual int getHitPoints() const { return hitPoints; }
virtual void changeHitPointsBy (int change) { hitPoints += change; }
virtual void displayChoices() = 0;
virtual const std::vector<Weapon*>& getWeaponsCarried() const { return weaponsCarried; }
virtual void addWeapon (Weapon* weapon) { weaponsCarried.push_back(weapon); }
virtual bool canUseWeapon (const Weapon*) const = 0;
virtual int checkForSpecialAttackBonus (Weapon*, const LivingBeing&) const = 0;
};
// Action classes
class Action {
protected:
LivingBeing* actionTaker;
public:
Action (LivingBeing* being) : actionTaker(being) { }
virtual ~Action() = default;
virtual void execute() = 0;
};
struct Option {
std::shared_ptr<Action> action;
std::string description;
Option (const std::shared_ptr<Action>& a, const std::string& d) : action(a), description(d) { }
bool operator< (const Option& other) const { return description < other.description; }
};
// Two Options will be considered equal if their description's are equal.
struct OptionPointerCompare {
bool operator()(const Option* a, const Option* b) const { return *a < *b; }
bool operator()(const std::shared_ptr<Option>& a, const std::shared_ptr<Option>& b) const { return operator()(a.get(), b.get()); }
};
class Attack : public Action {
public:
using Action::Action;
virtual void execute() override;
};
template <typename T>
class CastSpell : public Action {
public:
using Action::Action;
virtual void execute() override; // { dynamic_cast<T*>(actionTaker)->castASpell(); }
};
class TurnAwayUndead : public Action {
public:
using Action::Action;
virtual void execute() override; // { dynamic_cast<Cleric*>(actionTaker)->turnsAwayUndead(); }
};
class DoNothing : public Action {
public:
using Action::Action;
virtual void execute() override { std::cout << actionTaker->getName() << " does nothing.\n"; }
};
template <CharacterClassType> class Character;
using Fighter = Character<FIGHTER>;
using Wizard = Character<WIZARD>;
using Cleric = Character<CLERIC>;
class CharacterClass : public LivingBeing {
int level = 1;
CharacterClass* originalClass = nullptr; // Proxy Pattern. If *this is a new class added to a multiclass whose original class is originalClass, then *this is a proxy for originalClass in terms of hit points adjustment, weapons used, equipment carried, etc...
std::vector<CharacterClass*> otherClasses; // To handle multiclassing, using the Composite Pattern.
static const std::map<CharacterClassType, std::function<CharacterClass*(CharacterClass*)>> multiclassFactory;
public:
using LivingBeing::LivingBeing;
CharacterClass (CharacterClass* b) : originalClass(b) { } // Used only when multiclassing with a new CharacterClass type.
void levelUp();
virtual CharacterClassType getCharacterClassType() const = 0;
virtual std::string getName() const override { return originalClass ? originalClass->getName() : LivingBeing::getName(); } // PropertyProxy<T> can handle this automatically.
virtual int getHitPoints() const override { return originalClass ? originalClass->getHitPoints() : LivingBeing::getHitPoints(); }
virtual void changeHitPointsBy (int change) override { return originalClass ? originalClass->changeHitPointsBy(change) : LivingBeing::changeHitPointsBy(change); }
virtual const std::vector<Weapon*>& getWeaponsCarried() const override { return originalClass ? originalClass->getWeaponsCarried() : LivingBeing::getWeaponsCarried(); }
virtual void addWeapon (Weapon* weapon) override { return originalClass ? originalClass->addWeapon(weapon) : LivingBeing::addWeapon(weapon); }
bool isMultiClass() const { return !otherClasses.empty(); }
bool isMultiClassComponent() const { return originalClass != nullptr; } // i.e. *this is just a component class of originalClass.
int getMultiClassLevel() const { return level; }
int getLevel() const {
return !isMultiClass() ? level : level + std::accumulate(otherClasses.begin(), otherClasses.end(), 0,
[](int x, const CharacterClass* c) {return x + c->getMultiClassLevel();}); // level + Sum of all levels through otherClasses (Composite Pattern).
}
virtual void displayChoices() override;
// Testing checking whether a weapon can be used if a member of otherClasses can use it (even if *this cannot use it).
virtual bool canUseWeapon (const Weapon* weapon) const override {
const std::vector<CharacterClass*>& myClasses = isMultiClassComponent() ? originalClass->otherClasses : otherClasses;
// Composite Pattern, using canUse(const Weapon*) through otherClasses after using canUse on itself.
return canUse(weapon) || std::any_of(myClasses.begin(), myClasses.end(), [weapon](const CharacterClass* c)->bool { return c->canUse(weapon); });
}
virtual bool canUse (const Weapon* weapon) const = 0;
virtual std::string className() const = 0;
// Testing checking for bonus attack values among otherClasses (even if *this has no bonus attack value).
virtual int checkForSpecialAttackBonus (Weapon* weapon, const LivingBeing& target) const override {
int bonus = specialAttackBonus(weapon, target);
for (const CharacterClass* c : otherClasses) // Composite Pattern, using specialAttackBonus through otherClasses after using specialAttackBonus on itself.
bonus = std::max(bonus, c->specialAttackBonus(weapon, target));
return bonus;
}
protected:
virtual void gainNewLevelAttributes() = 0;
private:
virtual void getSpecificChoices (std::set<std::shared_ptr<Option>, OptionPointerCompare>&) = 0;
void increaseLevel() { level++; gainNewLevelAttributes(); }
void displayMulticlassLevels (std::ostream& = std::cout) const;
std::string characterClassName (CharacterClassType c) const { return std::unique_ptr<CharacterClass>(multiclassFactory.at(c)(nullptr))->className(); } // nullptr passed to prevent initializeAtLevelOne() being called.
virtual int specialAttackBonus (const Weapon*, const LivingBeing&) const = 0;
};
struct Weapon {
virtual std::string getName() const = 0;
bool canBeUsedBy (const CharacterClass*) const { return true; }
virtual bool canBeUsedBy (const Wizard*) const { return true; } // Overridden by Sword, which Wizards cannot use.
virtual bool canBeUsedBy (const Cleric*) const { return true; } // Overridden by SharpWeapon, which Clerics cannot use.
};
struct SharpWeapon : Weapon {
virtual bool canBeUsedBy (const Cleric*) const { return false; }
};
struct Sword : SharpWeapon {
std::string getName() const override { return "sword"; }
virtual bool canBeUsedBy (const Wizard*) const { return false; }
};
struct Dagger : SharpWeapon { std::string getName() const override { return "dagger"; } };
struct Mace : Weapon { std::string getName() const override { return "mace"; } };
template <>
class Character<FIGHTER> : public CharacterClass {
public:
Character (const std::string& name) : CharacterClass(name) { initializeAtLevelOne(); }
Character (CharacterClass* originalClass) : CharacterClass(originalClass) { if (originalClass) initializeAtLevelOne(); }
private:
virtual std::string className() const override { return "Fighter"; }
virtual CharacterClassType getCharacterClassType() const override { return FIGHTER; }
virtual void getSpecificChoices (std::set<std::shared_ptr<Option>, OptionPointerCompare>& choices) override {
choices.emplace(std::make_shared<Option>(std::make_shared<Attack>(this), "Attack"));
choices.emplace(std::make_shared<Option>(std::make_shared<DoNothing>(this), "Do nothing"));
}
void initializeAtLevelOne() { std::cout << getName() << " has been initialized as a level 1 fighter.\n"; }
virtual void gainNewLevelAttributes() override { std::cout << "New fighter attributes (e.g. weapon proficiencies) given to " << getName() << " after levelling up.\n"; }
bool canUse (const Weapon* weapon) const { return weapon->canBeUsedBy(this); } // double dispatch
virtual int specialAttackBonus (const Weapon* weapon, const LivingBeing&) const override {
if (getMultiClassLevel() >= 2 && dynamic_cast<const Sword*>(weapon) != nullptr) {
std::cout << "Special attack bonus of 3 given for Fighters at level 2+ with swords.\n";
return 3;
}
return 0;
}
};
template <>
class Character<WIZARD> : public CharacterClass {
int numSpells = 0;
public:
Character (const std::string& name) : CharacterClass(name) { initializeAtLevelOne(); }
Character (CharacterClass* originalClass) : CharacterClass(originalClass) { if (originalClass) initializeAtLevelOne(); }
void castASpell() {
std::cout << numSpells << " Wizard spells available, and one of them is cast.\n";
numSpells--;
std::cout << getName() << " has " << numSpells << " Wizard spells left.\n";
// The following CharacterClass:changeHitPointsBy(int) call is to demonstrate that originalClass's hit points (as opposed to the Wizard's hit points)
// is changed even if the Wizard is a multiclass but not the original class (Proxy Pattern).
std::cout << getName() << "'s fireball has caused damage to himself! 8 hit points lost!\n";
changeHitPointsBy(-8); // originalClass's hit points change (instead of the Wizard's hit points) if originalClass != nullptr.
std::cout << getName() << " has " << getHitPoints() << " hit points left.\n";
}
private:
virtual std::string className() const override { return "Wizard"; }
virtual CharacterClassType getCharacterClassType() const override { return WIZARD; }
virtual void getSpecificChoices (std::set<std::shared_ptr<Option>, OptionPointerCompare>& choices) override {
choices.emplace(std::make_shared<Option>(std::make_shared<Attack>(this), "Attack"));
choices.emplace(std::make_shared<Option>(std::make_shared<CastSpell<Wizard>>(this), "Cast a Wizard spell"));
choices.emplace(std::make_shared<Option>(std::make_shared<DoNothing>(this), "Do nothing"));
}
void initializeAtLevelOne() { numSpells = 3; std::cout << getName() << " has " << numSpells << " wizard spells.\n"; }
virtual void gainNewLevelAttributes() override { numSpells += 3; std::cout << getName() << " now has " << numSpells << " wizard spells.\n"; }
bool canUse (const Weapon* weapon) const { return weapon->canBeUsedBy(this); }
virtual int specialAttackBonus (const Weapon*, const LivingBeing&) const override { return 0; }
};
template <>
class Character<CLERIC> : public CharacterClass {
int numSpells = 0;
public:
Character (const std::string& name) : CharacterClass(name) { initializeAtLevelOne(); }
Character (CharacterClass* originalClass) : CharacterClass(originalClass) { if (originalClass) initializeAtLevelOne(); }
void castASpell() {
std::cout << numSpells << " Cleric spells available, and one of them is cast.\n";
numSpells--;
std::cout << getName() << " has " << numSpells << " Cleric spells left.\n";
}
void turnsAwayUndead() { std::cout << getName() << " turns away all undead present.\n"; }
private:
virtual std::string className() const override { return "Cleric"; }
virtual CharacterClassType getCharacterClassType() const override { return CLERIC; }
virtual void getSpecificChoices (std::set<std::shared_ptr<Option>, OptionPointerCompare>& choices) override {
choices.emplace(std::make_shared<Option>(std::make_shared<Attack>(this), "Attack"));
choices.emplace(std::make_shared<Option>(std::make_shared<CastSpell<Cleric>>(this), "Cast a Cleric spell"));
choices.emplace(std::make_shared<Option>(std::make_shared<TurnAwayUndead>(this), "Turn away undead"));
choices.emplace(std::make_shared<Option>(std::make_shared<DoNothing>(this), "Do nothing"));
}
void initializeAtLevelOne() { numSpells = 2; std::cout << getName() << " has " << numSpells << " cleric spells.\n"; }
virtual void gainNewLevelAttributes() override { numSpells += 2; std::cout << getName() << " now has " << numSpells << " cleric spells.\n"; }
bool canUse (const Weapon* weapon) const { return weapon->canBeUsedBy(this); }
virtual int specialAttackBonus (const Weapon*, const LivingBeing&) const override { return 0; }
};
const std::map<CharacterClassType, std::function<CharacterClass*(CharacterClass*)>> CharacterClass::multiclassFactory = {
{ FIGHTER, [](CharacterClass* originalClass) { return new Fighter(originalClass); } },
{ WIZARD, [](CharacterClass* originalClass) { return new Wizard(originalClass); } },
{ CLERIC, [](CharacterClass* originalClass) { return new Cleric(originalClass); } }
};
void CharacterClass::levelUp() {
changeHitPointsBy(5);
std::cout << '\n' << getName() << " has leveled up, and is now has " << getHitPoints() << " hit points.\n";
if (otherClasses.size() == NUM_CHARACTER_TYPES - 1 || ask_yn("Does " + getName() + " wish to multiclass with a new class as he levels up?") == false) {
// No new class is being added, but if *this is already a multiclass, we must ask which class he wants to level up in.
if (otherClasses.size() > 0) {
std::vector<CharacterClass*> choices;
int choiceNumber = 2;
std::cout << "Which class does " << getName() << " wish to level up in?\n";
std::cout << "1. " << className() << '\n'; // The original class
for (CharacterClass* c : otherClasses) {
std::cout << choiceNumber++ << ". " << c->className() << '\n';
choices.push_back(c);
}
std::cin >> choiceNumber;
if (choiceNumber == 1)
increaseLevel();
else
choices[choiceNumber-2]->increaseLevel();;
std::cout << getName() << "'s new levels are:\n";
displayMulticlassLevels();
}
else {
increaseLevel();
std::cout << getName() << "' is now at level " << level << ".\n";
}
return;
}
std::cout << "What new class does " << getName() << " with to multiclass with?\n";
std::vector<CharacterClassType> characterClassChoices;
int choiceNumber = 1;
for (int i = 0; i < NUM_CHARACTER_TYPES; ++i) {
const CharacterClassType c = static_cast<CharacterClassType>(i);
if (c == getCharacterClassType())
continue;
if (std::none_of(otherClasses.begin(), otherClasses.end(), [c](const CharacterClass* cc)->bool { return cc->getCharacterClassType() == c; })) {
characterClassChoices.push_back(c);
std::cout << choiceNumber++ << ". " << characterClassName(c) << '\n';
}
}
std::cin >> choiceNumber;
const CharacterClassType type = characterClassChoices[choiceNumber-1];
CharacterClass* newClass = multiclassFactory.at(type)(this); // 'this' is passed and set as the 'originalClass' data member of newClass.
otherClasses.push_back(newClass);
std::cout << getName() << "'s new levels are:\n";
displayMulticlassLevels();
}
void CharacterClass::displayChoices() {
// Some Options will be identical among 'otherClasses', e.g. Attack or DoNothing option, so we must avoid the repeitions using std::set<std::shared_ptr<Option>, OptionPointerCompare>.
std::set<std::shared_ptr<Option>, OptionPointerCompare> choices;
std::cout << "\nWhat does " << getName() << " wish to do?\n";
getSpecificChoices(choices);
for (CharacterClass* c : otherClasses)
c->getSpecificChoices(choices);
std::vector<std::shared_ptr<Option>> menu(choices.begin(), choices.end());
int choiceNumber = 1;
for (const auto& c : choices)
std::cout << choiceNumber++ << ". " << c->description << '\n';
std::cin >> choiceNumber;
menu[choiceNumber-1]->action->execute();
}
void CharacterClass::displayMulticlassLevels (std::ostream& os) const {
os << className() << ": level " << level << '\n'; // Original class
for (const CharacterClass* c : otherClasses)
os << c->className() << ": level " << c->level << '\n';
std::cout << "Total level: " << getLevel() << '\n';
}
void Attack::execute() {
Fighter enemy("Enemy"); // Suppose 'enemy' is the target.
std::cout << "Which weapon does " << actionTaker->getName() << " wish to use to attack " << enemy.getName() << "?\n";
for (const Weapon* weapon : actionTaker->getWeaponsCarried()) {
if (actionTaker->canUseWeapon(weapon)) // If is a multiclass, actionTaker's 'otherClasses' are checked if 'weapon' can be used, even if 'actionTaker' himself cannot use it.
std::cout << weapon->getName() << '\n';
}
Weapon* weaponUsed = actionTaker->getWeaponsCarried().back(); // Going simple for now.
const int attackBonus = actionTaker->checkForSpecialAttackBonus(weaponUsed, enemy); // If is a multiclass, actionTaker's 'otherClasses' are checked as well as 'actionTaker' himself.
if (attackBonus != 0)
std::cout << "Attack bonus of " << attackBonus << " used.\n";
// And similarly for other special attributes from actionTaker's 'otherClasses', if actionTaker is a multiclass.
}
template <typename T>
void CastSpell<T>::execute() { dynamic_cast<T*>(actionTaker)->castASpell(); }
void TurnAwayUndead::execute() { dynamic_cast<Cleric*>(actionTaker)->turnsAwayUndead(); }
void test (CharacterClass& c) {
c.addWeapon(new Dagger);
c.addWeapon(new Mace);
c.addWeapon(new Sword);
c.levelUp();
c.levelUp();
c.displayChoices();
c.levelUp();
c.displayChoices();
c.displayChoices();
}
int main() {
// Fighter fighter("Rex");
// test(fighter);
Wizard wizard("Merlin");
test(wizard);
// Cleric cleric("Luther");
// test(cleric);
}
Sample output:
Merlin has 3 wizard spells.
Merlin has leveled up, and is now has 15 hit points.
Does Merlin wish to multiclass with a new class as he levels up? (y/n) y
What new class does Merlin with to multiclass with?
1. Fighter
2. Cleric
1
Merlin has been initialized as a level 1 fighter.
Merlin's new levels are:
Wizard: level 1
Fighter: level 1
Total level: 2
Merlin has leveled up, and is now has 20 hit points.
Does Merlin wish to multiclass with a new class as he levels up? (y/n) y
What new class does Merlin with to multiclass with?
1. Cleric
1
Merlin has 2 cleric spells.
Merlin's new levels are:
Wizard: level 1
Fighter: level 1
Cleric: level 1
Total level: 3
What does Merlin wish to do?
1. Attack
2. Cast a Cleric spell
3. Cast a Wizard spell
4. Do nothing
5. Turn away undead
2
2 Cleric spells available, and one of them is cast.
Merlin has 1 Cleric spells left.
Merlin has leveled up, and is now has 25 hit points.
Which class does Merlin wish to level up in?
1. Wizard
2. Fighter
3. Cleric
1
Merlin now has 6 wizard spells.
Merlin's new levels are:
Wizard: level 2
Fighter: level 1
Cleric: level 1
Total level: 4
What does Merlin wish to do?
1. Attack
2. Cast a Cleric spell
3. Cast a Wizard spell
4. Do nothing
5. Turn away undead
1
Enemy has been initialized as a level 1 fighter.
Which weapon does Merlin wish to use to attack Enemy?
dagger
mace
sword
What does Merlin wish to do?
1. Attack
2. Cast a Cleric spell
3. Cast a Wizard spell
4. Do nothing
5. Turn away undead
3
6 Wizard spells available, and one of them is cast.
Merlin has 5 Wizard spells left.
Merlin's fireball has caused damage to himself! 8 hit points lost!
Merlin has 17 hit points left.
Note that the overrides in CharacterClass::
virtual std::string getName() const override { return originalClass ? originalClass->getName() : LivingBeing::getName(); }
virtual int getHitPoints() const override { return originalClass ? originalClass->getHitPoints() : LivingBeing::getHitPoints(); }
virtual void changeHitPointsBy (int change) override { return originalClass ? originalClass->changeHitPointsBy(change) : LivingBeing::changeHitPointsBy(change); }
are a nuisance, though necessary. They can be handled automatically and the need for them being virtual removed by using a PropertyProxy class for those data members:
template <typename T>
struct PropertyProxy {
using F = T(LivingBeing::*)() const; // T get method
using G = void(LivingBeing::*)(const T&); // const T& set method
T value;
LivingBeing* being;
F getPtrFunc;
G setPtrFunc;
PropertyProxy (const T initial_value) { *this = initial_value; }
PropertyProxy (LivingBeing* b, F f) : being(b), getPtrFunc(f) { }
PropertyProxy (LivingBeing* b, F f, G g) : being(b), getPtrFunc(f), setPtrFunc(g) { }
PropertyProxy (const T initial_value, LivingBeing* b, F f) : value(initial_value), being(b), getPtrFunc(f) { }
PropertyProxy (const T initial_value, LivingBeing* b, F f, G g) : value(initial_value), being(b), getPtrFunc(f), setPtrFunc(g) { }
PropertyProxy() = default;
operator T() const;
T operator= (const T&);
void operator+= (const T& t) { *this = *this + t; }
void operator-= (const T& t) { *this = *this - t; }
friend T operator+ (const T& t, const PropertyProxy<T>& p) { return t + p.value; }
friend T operator- (const T& t, const PropertyProxy<T>& p) { return t - p.value; }
};
with
template <typename T>
PropertyProxy<T>::operator T() const {
const CharacterClass* characterClass = dynamic_cast<const CharacterClass*>(being);
if (!characterClass)
return value;
return characterClass->isMultiClassComponent() ? (characterClass->getOriginalClass()->*getPtrFunc)() : value;
}
template <typename T>
T PropertyProxy<T>::operator= (const T& new_value) {
const CharacterClass* characterClass = dynamic_cast<const CharacterClass*>(being);
if (!characterClass)
return value = new_value;
if (characterClass->isMultiClassComponent()) {
(characterClass->getOriginalClass()->*setPtrFunc)(new_value);
return new_value;
}
return value = new_value;
}
Then changing the name and hitPoints data member of LivingBeing to
PropertyProxy<std::string> name{this, &LivingBeing::getName, &LivingBeing::setName};
PropertyProxy<int> hitPoints{this, &LivingBeing::getHitPoints, &LivingBeing::setHitPoints};
will do the job. However, this property proxy only works for primitive types, and not for types like std::vector and such that have their own methods. Hence, without having the full benefit desired, I decided to not use it at all and leave the original unchanged.
I should also mention that above is dynamic multiclassing, as opposed to static multiclassing: using Multiclass<OriginalClass, Ts...>
, inheriting from OriginalClass, Ts...
, is an example of the
Static Decorator Pattern, and extreme case in fact, in the sense that each of OriginalClass, Ts... are decorated with data and functionality of all the other classes. Whenever a CharacterClass (which might already be a multiclass) decides to multiclass with a new type of CharacterClass, it is being decorated with the data and methods of that new type of CharacterClass. But using the static decorator pattern, as opposed to the dynamic decorator pattern, has its drawbacks, as a CharacterClass that is multiclassing will have to be turned in to a new type, and all pointers to the old type (which should be destroyed) will become invalid. Furthermore, the coding for it required so much template metaprogramming (and even multiple dispatching) that I didn't bother to show it for comparison. It is just a plain bad choice, and full of so many bugs that I gave up on trying to perfect it. The dynamic version I firmly believe is much better, and definitely much simpler.
CharacterClas
has astd::vector<CharacterClass*>
member, with each element iterated through for some methods I illustrated, e.g.canUse(Weapon*)
andspecialAttackBonus()
. If the original character cannot use a sword, but one of the multiclass components can, then the overall character can use a sword. My sample output showed that (Merlin initially could not use a sword until multiclassing with a Fighter). \$\endgroup\$