3
\$\begingroup\$

Races have many distinguishing physical characteristics. But apart from appearances, different races have other distinguishing characteristcs, all of I've decided to be built using the Builder Pattern. Separate builder classes I found to very useful when many races have multiple sub-races as well. Once the races and sub-races are defined in the code below, the accompanying builder classes are defined and then used in the constructors of the HumanoidWithRaceTypes.

#include <iostream>
#include <string>
#include <unordered_map>
#include <fstream>

const int NOT_FOUND = -1;

enum RaceType {HUMAN, ELF, DWARF, HALFLING};
enum HumanSubRaceType {CALASHITE, CHONDATHAN, DAMARAN};  // several more
enum ElfSubRaceType {HIGH, WOOD, DROW};
enum HighElfSubRaceType {GREY, VALLEY, SILVANESTI, GREYHAWK_HIGH, QUALINESTI, MOON, SUN};
enum WoodElfSubRaceType {WILD, KAGONESTI, GREYHAWK_WOOD, FORGOTTEN_REALMS_WOOD, FOREST};

class Race;
class HumanoidWithRaceType;

template <RaceType> class RaceClass;
using Human = RaceClass<HUMAN>;
using Elf = RaceClass<ELF>;

template <HumanSubRaceType> class HumanSubRace;
using Calashite = HumanSubRace<CALASHITE>;
using Chondathan = HumanSubRace<CHONDATHAN>;
using Damaran = HumanSubRace<DAMARAN>;
// etc... for the other Human subraces

template <ElfSubRaceType> class ElfSubRace;
using HighElf = ElfSubRace<HIGH>;
using WoodElf = ElfSubRace<WOOD>;
using Drow = ElfSubRace<DROW>;

template <HighElfSubRaceType> class HighElfSubRace;
using GreyElf = HighElfSubRace<GREY>;
using ValleyElf = HighElfSubRace<VALLEY>;
using SilvanestiElf = HighElfSubRace<SILVANESTI>;
using GreyhawkHighElf = HighElfSubRace<GREYHAWK_HIGH>;
using QualinestiElf = HighElfSubRace<QUALINESTI>;
using MoonElf = HighElfSubRace<MOON>;
using SunElf = HighElfSubRace<SUN>;

template <WoodElfSubRaceType> class WoodElfSubRace;
using WildElf = WoodElfSubRace<WILD>;
using KagonestiElf = WoodElfSubRace<KAGONESTI>;
using GreyhawkWoodElf = WoodElfSubRace<GREYHAWK_WOOD>;
using ForgottenRealmsWoodElf = WoodElfSubRace<FORGOTTEN_REALMS_WOOD>;
using ForestElf = WoodElfSubRace<FOREST>;

class LivingBeing {
    int intelligence = 9, wisdom = 9, dexterity = 9, height = 180;
    int hitPoints = 10;
public:
    void changeIntelligenceBy (int change) { intelligence += change; }
    void changeWisdomBy (int change) { wisdom += change; } 
    void changeDexterityBy (int change) { dexterity += change; }
    void changeHeightBy (int change) { height += change; }
    void setHitPoints (int hp) { hitPoints = hp; }
    virtual bool notAffectedBySleepSpell() const { return false; }  // Overriden by UndeadMonster and HumanoidWithRaceType with race == any kind of Elf.
    virtual bool notAffectedByCharmSpell() const { return false; }  // Overriden by UndeadMonster and HumanoidWithRaceType with race == any kind of Elf.
    virtual bool hasAdvantageVsCharm() const { return false; }  // Overriden by HumanoidWithRaceType with race == any kind of Elf.
    virtual void print (std::ostream& os = std::cout) const {
        os << "Intelligence: " << intelligence << '\n';
        os << "Wisdom: " << wisdom << '\n';
        os << "Dexterity: " << dexterity << '\n';
        os << "Height: " << height << '\n';
        os << "Hit Points: " << hitPoints << '\n';
    }
    virtual void save (std::ostream& os) const { os << intelligence << ' ' << wisdom << ' ' << dexterity << ' ' << height << ' ' << hitPoints << '\n'; }
    virtual void load (std::istream& is) { is >> std::skipws >> intelligence >> wisdom >> dexterity >> height >> hitPoints; }
};

struct Weapon {
    virtual std::string getName() const = 0;
};
struct LongSword : Weapon { std::string getName() const override { return "long sword"; } };
struct ShortSword : Weapon { std::string getName() const override { return "short sword"; } };
struct Armor {
    virtual std::string getName() const = 0;
};
struct SmallChainMail : Armor { std::string getName() const override { return "small chainmail"; } };
struct LargeChainMail : Armor { std::string getName() const override { return "large chainmail"; } };

// DemiHuman is a being that has physical features of a human being and can use weapons and/or wear armor, which not only includes Humanoids (like humans-sized Goblins, Orcs, etc...) but also certain undead, giants, celestials, even deities, all of which use weapons and wear armor.
class DemiHuman : public LivingBeing {
    Weapon* weaponInHand = nullptr;
    Armor* armorWorn = nullptr;
    int proficiencyWithMissiles = 5;
public:
    Weapon* getWeapon() const { return weaponInHand; }
    void setWeapon (Weapon* weapon) { weaponInHand = weapon; }
    void setArmor (Armor* armor) { armorWorn = armor; }
    void changeProficiencyWithMissilesBy (int change) { proficiencyWithMissiles += change; }
    virtual void save (std::ostream& os) const override {
        LivingBeing::save(os);
        os << proficiencyWithMissiles << '\n';  // Skipping weaponInHand and armorWorn for simplicity.
        if (weaponInHand) {
            os << std::boolalpha << true << '\n';
            os << weaponInHand->getName() << '\n';
        }
        else
            os << std::boolalpha << false << '\n';
        if (armorWorn) {
            os << std::boolalpha << true << '\n';
            os << armorWorn->getName() << '\n';
        }
        else
            os << std::boolalpha << false << '\n';
    }
    virtual void load (std::istream& is) override {
        LivingBeing::load(is);
        is >> proficiencyWithMissiles;  // Skipping weaponInHand and armorWorn for simplicity.
        bool b;
        std::string itemName;
        is >> std::boolalpha >> b;
        if (b) {
            while (std::getline(is, itemName) && itemName.empty());
            if (itemName == "short sword")  // Factory std::map<std::string, std::function<Weapon*()>> construction and look-up omitted for brevity.
                weaponInHand = new ShortSword;
        }
        is >> std::boolalpha >> b;
        if (b) {
            while (std::getline(is, itemName) && itemName.empty());
            if (itemName == "small chainmail")  // Factory std::map<std::string, std::function<Armor*()>> construction and look-up omitted for brevity.
                armorWorn = new SmallChainMail;
        }
    }
    virtual void print (std::ostream& os = std::cout) const override {
        LivingBeing::print(os);
        if (weaponInHand) os << "Holding " << weaponInHand->getName() << " as a weapon.\n";
        else os << "No weapon in hand.\n";
        if (armorWorn) os << "Wearing " << armorWorn->getName() << " for armor.\n";
        else os << "No armor worn.\n";
        os << "Proficiency With missiles = " << proficiencyWithMissiles << '\n';
    }
};

class Humanoid : public DemiHuman { };  // Includes Goblins, Orcs, Kobolds, etc... (all human-sized)

class RaceDatabase {
    static std::unordered_map<std::string, Race*> map;  // Used for saving and loading DemiHumanWithRaceType instances from strings.
public:
    static Race* getRaceFromString (const std::string& raceName) { return map.at(raceName); }
    static void insertIntoMap (const std::string& tag, Race* race) { map.emplace(tag, race);  std::cout << "{" << tag << ", " << race << "} inserted in RaceDatabase::map.\n"; }
private:
    RaceDatabase() = default;  RaceDatabase (const RaceDatabase&);  const RaceDatabase& operator = (const RaceDatabase&);  ~RaceDatabase() = default;
};
std::unordered_map<std::string, Race*> RaceDatabase::map;

class Race {
protected:
    struct Exemplar {};
public:
    virtual void build (HumanoidWithRaceType&) = 0;
    virtual std::string tag() const = 0;
    virtual int getWalkingSpeed() const = 0;
    virtual bool isElf() const { return false; }
    virtual int getDarkVisionDistance() const { return NOT_FOUND; }
    virtual bool notAffectedBySleepSpell() const { return false; }  // overridden by Elf
    virtual bool hasAdvantageVsCharm() const { return false; }  // overridden by Elf
//      virtual void increaseNumCantripsByOne (SpellCasterCharacterClass*) const { }  // overridden by HighElf only
};

template <RaceType>
class RaceCRTP : public Race {
    virtual void build (HumanoidWithRaceType&) override;
};

// Human and its subraces
template <>
class RaceClass<HUMAN> : public RaceCRTP<HUMAN> {  // Human
    static const Human prototype;
public:
    static Human* getInstance() {static Human* instance = new Human;  return instance;}  // Singleton, because all beings of this (or any other) race shall simply share the same Human* pointer (Flyweight Pattern).
protected:  // The default constructor, copy constructor, assignment operator cannot be accessed in order to force the sharing of the same Human* pointer by all beings of race Human.
    RaceClass() = default;  RaceClass (const RaceClass&);  const RaceClass& operator = (const RaceClass&);  ~RaceClass() = default;
private:
    RaceClass (Exemplar) { RaceDatabase::insertIntoMap(className(), getInstance()); }  // Cannot implement this in the base class Race because getInstance() is being passed instead of 'this' (and virtual functions should not be called in a base constructor).
    virtual std::string tag() const override { return className(); }
    static std::string className() { return "Human"; }
    virtual int getWalkingSpeed() const override { return 30; }
};
const Human Human::prototype(Exemplar{});


template <HumanSubRaceType>
class HumanCRTP : public Human {
    // Whatever is needed using CRTP, other than build(HumanoidWithRaceType&).
};

template <HumanSubRaceType H>
class HumanCRTPWithBuilder : public HumanCRTP<H> {
    virtual void build (HumanoidWithRaceType&) override;  // No other methods defined here.
};

template <>
class HumanSubRace<CALASHITE> : public HumanCRTPWithBuilder<CALASHITE> {  // Calashite
    static const Calashite prototype;
public:
    static Calashite* getInstance() {static Calashite* instance = new Calashite;  return instance;}  // Singleton
private:
    HumanSubRace (Exemplar) { RaceDatabase::insertIntoMap(className(), getInstance()); }  // Cannot implement this in the base class Race because getInstance() is being passed instead of 'this' (and virtual functions should not be called in a base constructor).
    HumanSubRace() = default;  HumanSubRace (const HumanSubRace&);  const HumanSubRace& operator = (const HumanSubRace&);  ~HumanSubRace() = default;
    virtual std::string tag() const override { return className(); }
    static std::string className() { return "Calashite Human"; }
};
const Calashite Calashite::prototype(Exemplar{});

// HumanBuilder<CHONDATHAN> is not to be defined because it won't do anything different from RacialClassBuilder<HUMAN>, i.e. being of race Chondathan Human is no different from Human from a builder point of view,
// so HumanSubRace<CHONDATHAN> will just derive from HumanCRTP<CHONDATHAN> instead of from HumanCRTPWithBuilder<CHONDATHAN>.
template <>
class HumanSubRace<CHONDATHAN> : public HumanCRTP<CHONDATHAN> {  // Chondathan
    static const Chondathan prototype;
public:
    static Chondathan* getInstance() {static Chondathan* instance = new Chondathan;  return instance;}  // Singleton
private:
    HumanSubRace (Exemplar) { RaceDatabase::insertIntoMap(className(), getInstance()); }
    HumanSubRace() = default;  HumanSubRace (const HumanSubRace&);  const HumanSubRace& operator = (const HumanSubRace&);  ~HumanSubRace() = default;
    virtual std::string tag() const override { return className(); }
    static std::string className() { return "Chondathan Human"; }
};
const Chondathan Chondathan::prototype(Exemplar{});

template <>
class HumanSubRace<DAMARAN> : public HumanCRTP<DAMARAN> {  // Damaran (nothing special about it either from a builder point of view, so also derive from HumanCRTP<DAMARAN> instead of from HumanCRTPWithBuilder<DAMARAN>).
        static const Damaran prototype;
    public:
        static Damaran* getInstance() {static Damaran* instance = new Damaran;  return instance;}  // Singleton
    private:
        HumanSubRace (Exemplar) {RaceDatabase::insertIntoMap (className(), getInstance());}
        HumanSubRace() = default;  HumanSubRace (const HumanSubRace&);  const HumanSubRace& operator = (const HumanSubRace&);  ~HumanSubRace() = default;
        virtual std::string tag() const override {return className();}
        static std::string className() {return "Damaran Human";}
};
const Damaran Damaran::prototype(Exemplar{});
//  and several more Human subraces

// Elf and its subraces
template <>
class RaceClass<ELF> : public RaceCRTP<ELF> {  // Elf
    static const Elf prototype;
public:
    static Elf* getInstance() {static Elf* instance = new Elf;  return instance;}  // Singleton
protected:
    RaceClass() = default;  RaceClass (const RaceClass&);  const RaceClass& operator = (const RaceClass&);  ~RaceClass() = default;
private:
    RaceClass (Exemplar) { RaceDatabase::insertIntoMap(className(), getInstance()); }
    virtual bool isElf() const override { return true; }
    virtual std::string tag() const override { return className(); }
    static std::string className() { return "Elf"; }
    virtual int getWalkingSpeed() const override { return 32; }
    virtual bool notAffectedBySleepSpell() const override { return true; }
    virtual bool hasAdvantageVsCharm() const override { return true; }
};
const Elf Elf::prototype(Exemplar{});

template <ElfSubRaceType>
class ElfCRTP : public Elf {
    // Whatever is needed using CRTP, other than build(HumanoidWithRaceType&).
};

template <ElfSubRaceType E>
class ElfCRTPWithBuilder : public ElfCRTP<E> {
    virtual void build (HumanoidWithRaceType&) override;  // No other methods defined here.
};

template <>
class ElfSubRace<HIGH> : public ElfCRTPWithBuilder<HIGH> {  // HighElf
    static const HighElf prototype;
public:
    static HighElf* getInstance() { static HighElf* instance = new HighElf;  return instance; }  // Singleton
protected:  // ElfSubRace<HIGH> will itself have subraces deriving from it.
    ElfSubRace() = default;  ElfSubRace (const ElfSubRace&);  const ElfSubRace& operator = (const ElfSubRace&);  ~ElfSubRace() = default;
private:
    ElfSubRace (Exemplar) { RaceDatabase::insertIntoMap(className(), getInstance()); }
    virtual std::string tag() const override { return className(); }
    static std::string className() { return "High Elf"; }
};
const HighElf HighElf::prototype(Exemplar{});

template <HighElfSubRaceType>
class HighElfCRTP : public HighElf {
    // Whatever is needed using CRTP, other than build(HumanoidWithRaceType&).
};

template <HighElfSubRaceType H>
class HighElfCRTPWithBuilder : public HighElfCRTP<H> {
    virtual void build (HumanoidWithRaceType&) override;  // No other methods defined here.
};

template <>
class HighElfSubRace<GREY> : public HighElfCRTPWithBuilder<GREY> {  // GreyElf
    static const GreyElf prototype;
public:
    static GreyElf* getInstance() { static GreyElf* instance = new GreyElf;  return instance; }  // Singleton
private:
    HighElfSubRace (Exemplar) { RaceDatabase::insertIntoMap(className(), getInstance()); }
    HighElfSubRace() = default;  HighElfSubRace (const HighElfSubRace&);  const HighElfSubRace& operator = (const HighElfSubRace&);  ~HighElfSubRace() = default;
    virtual std::string tag() const override { return className(); }
    static std::string className() { return "Grey Elf"; }
};
const GreyElf GreyElf::prototype(Exemplar{});

template <>
class HighElfSubRace<VALLEY> : public HighElfCRTPWithBuilder<VALLEY> {  // ValleyElf
    static const ValleyElf prototype;
public:
    static ValleyElf* getInstance() { static ValleyElf* instance = new ValleyElf;  return instance; }  // Singleton
private:
    HighElfSubRace (Exemplar) { RaceDatabase::insertIntoMap(className(), getInstance()); }
    HighElfSubRace() = default;  HighElfSubRace (const HighElfSubRace&);  const HighElfSubRace& operator = (const HighElfSubRace&);  ~HighElfSubRace() = default;
    virtual std::string tag() const override { return className(); }
    static std::string className() { return "Valley Elf"; }
};
const ValleyElf ValleyElf::prototype(Exemplar{});

// ***** The all-important derived type of Humanoid that has races.
class HumanoidWithRaceType : public Humanoid {  // This class placed between CharacterClass and Humanoid to accomodate things like Goblin races, Orc races, Kobold races, etc...
    Race* race;  // Type Object Pattern (better choice than using multiple inheritance from Race).
public:
    HumanoidWithRaceType (Race* race) { setRace(race); }
    HumanoidWithRaceType() = default;
    Race* getRace() const { return race; }
    virtual void setRace (Race* thisRace) { race = thisRace;  race->build(*this); }  // ***** 'race->build(*this);' is the key line that initializes all the stats of 'this' according to what 'race' is.  No need for the Visitor Pattern, and the 'build(HumanoidWithRaceType&)' overrides do actually belong in the Race subtypes (just one override for each concrete derived class of Race), and hence the Open-Closed Principle is not violated. 
    int getDarkVisionDistance() const { return race->getDarkVisionDistance(); }
    virtual void save (std::ostream& os) const override {
        DemiHuman::save(os);
        os << race->tag() << '\n';
    }
    virtual void load (std::istream& is) override {
        DemiHuman::load(is);
        std::string raceName;
        while (std::getline(is, raceName) && raceName.empty());
        race = RaceDatabase::getRaceFromString(raceName);  // RaceDatabase now being used.  No need to call 'setRace', namely 'race->build(*this);' (which is only used for Factory-type production of HumanoidWithRaceType's) since all the special attributes have been saved and hence will be loaded. 
    }
    virtual void print (std::ostream& os = std::cout) const override {
        DemiHuman::print(os);
        os << "Race: " << race->tag() << '\n';
    }
private:
    virtual bool notAffectedBySleepSpell() const override { return race->notAffectedBySleepSpell(); }  // Elves resist with 100% success.
    virtual bool hasAdvantageVsCharm() const override { return race->hasAdvantageVsCharm(); }  // Elves have advantage saving throw vs Charm spells.
};

// Abstract builder classes
class HumanoidWithRaceTypeBuilder {
    HumanoidWithRaceType& being;  // Reference is used here because a HumanoidWithRaceType already built by some other builder (e.g. PrimaryStatsBuilder) is passed into here, instead of being instantiated here as a pointer (as the classic builder pattern follows).
public:
    HumanoidWithRaceTypeBuilder (HumanoidWithRaceType& d) : being(d) {}
protected:
    HumanoidWithRaceType& getBeing() const { return being; }
};

class RacialClassBuilderBase : protected HumanoidWithRaceTypeBuilder {
    friend class RacialClassDirector;
public:
    using HumanoidWithRaceTypeBuilder::HumanoidWithRaceTypeBuilder;
protected:
    virtual void adjustAbilityScores() const = 0;
    virtual void adjustWeaponProficiencies() const = 0;
    virtual void adjustPhysicalAppearance() const = 0;
};

// The director class in the Builder Pattern.
class RacialClassDirector {
private:
    const RacialClassBuilderBase& racialClassBuilderBase;  // const, as setRacialClassBuilderBase(RacialClassBuilderBase*) is not needed I don't think, since this class is apparently only used the 'build' overides in Race subtypes.
public:
    RacialClassDirector (const RacialClassBuilderBase& r) : racialClassBuilderBase(r) {}
    void construct() const {  // The key Facade Pattern used in the actual building process in the Builder Pattern.
        racialClassBuilderBase.adjustAbilityScores();
        racialClassBuilderBase.adjustWeaponProficiencies();
        racialClassBuilderBase.adjustPhysicalAppearance();
//      racialClassBuilderBase.adjustLifespan();  // And common diseases, etc...
        // etc...
    }
};

// The concrete race builder classes
template <RaceType> class RacialClassBuilder;

template <>
class RacialClassBuilder<HUMAN> : public RacialClassBuilderBase {
public:
    using RacialClassBuilderBase::RacialClassBuilderBase;
protected:
    virtual void adjustAbilityScores() const override { }  // Do whatever applies for all humans.
    virtual void adjustWeaponProficiencies() const override { }  // Do whatever applies for all humans.
    virtual void adjustPhysicalAppearance() const override { }  // Do whatever applies for all humans.
};

template <HumanSubRaceType> class HumanBuilder;

template <>
class HumanBuilder<CALASHITE>: public RacialClassBuilder<HUMAN> {
public:
    using RacialClassBuilder<HUMAN>::RacialClassBuilder;
protected:
    virtual void adjustAbilityScores() const override {
        RacialClassBuilder<HUMAN>::adjustAbilityScores();
        getBeing().changeIntelligenceBy(2);
    }
    virtual void adjustWeaponProficiencies() const override {
        RacialClassBuilder<HUMAN>::adjustWeaponProficiencies();
        getBeing().changeProficiencyWithMissilesBy(1);  
    }
    virtual void adjustPhysicalAppearance() const override {
        RacialClassBuilder<HUMAN>::adjustPhysicalAppearance();
        getBeing().changeHeightBy(7);   
    }
};

template <>
class RacialClassBuilder<ELF> : public RacialClassBuilderBase {
public:
    using RacialClassBuilderBase::RacialClassBuilderBase;
protected:
    virtual void adjustAbilityScores() const override { getBeing().changeDexterityBy(2); }
    virtual void adjustWeaponProficiencies() const override { }  // Do whatever applies for all elves.
    virtual void adjustPhysicalAppearance() const override { getBeing().changeHeightBy(-30); }  // Elves are much shorter than humans.
};

template <ElfSubRaceType> class ElfBuilder;

template <>
class ElfBuilder<HIGH>: public RacialClassBuilder<ELF> {
public:
    using RacialClassBuilder<ELF>::RacialClassBuilder;
protected:
    virtual void adjustAbilityScores() const override {
        RacialClassBuilder<ELF>::adjustAbilityScores();
        getBeing().changeIntelligenceBy(1);
//      SpellCasterCharacterClass* spellCaster = dynamic_cast<SpellCasterCharacterClass*>(&getBeing());
//      if (spellCaster)
//          spellCaster->increaseNumCantripsByOne();
    }
    virtual void adjustWeaponProficiencies() const override {
        RacialClassBuilder<ELF>::adjustWeaponProficiencies();
        getBeing().changeProficiencyWithMissilesBy(2);
    }
};

template <HighElfSubRaceType> class HighElfBuilder;

template <>
class HighElfBuilder<GREY>: public ElfBuilder<HIGH> {  //  Haughty, reclusive, believing themselves to be superior to non-elves and even other elves.
public:
    using ElfBuilder<HIGH>::ElfBuilder;
protected:
    virtual void adjustAbilityScores() const override {
        ElfBuilder<HIGH>::adjustAbilityScores();
        getBeing().changeWisdomBy(1);
    }
    virtual void adjustWeaponProficiencies() const override {
        ElfBuilder<HIGH>::adjustWeaponProficiencies();
        getBeing().changeProficiencyWithMissilesBy(1);  // Grey Elves are even better with missiles than the already missile-adept Elf.
    }
    virtual void adjustPhysicalAppearance() const override {
        ElfBuilder<HIGH>::adjustPhysicalAppearance();
        getBeing().changeHeightBy(-10);  // Grey Elves are 10 cm shorter than the already short Elf.
    }
};

template <>
class HighElfBuilder<VALLEY>: public ElfBuilder<HIGH> {
public:
    using ElfBuilder<HIGH>::ElfBuilder;
protected:
    virtual void adjustAbilityScores() const override {
        ElfBuilder<HIGH>::adjustAbilityScores();
        // Add whatever unique to ValleyElf.
    }
    virtual void adjustWeaponProficiencies() const override {
        ElfBuilder<HIGH>::adjustWeaponProficiencies();
        // Add whatever unique to ValleyElf.
    }
    virtual void adjustPhysicalAppearance() const override {
        ElfBuilder<HIGH>::adjustPhysicalAppearance();
        // Add whatever unique to ValleyElf.
    }
};
// Similarly for class HighElfBuilder<SILVANESTI>: public ElfBuilder<HIGH>, etc...

// The all-important build(HumanoidWithRaceType&) overrides of the Race class.
template <RaceType R>
void RaceCRTP<R>::build (HumanoidWithRaceType& h) {
    const RacialClassBuilder<R> builder(h);
    const RacialClassDirector director(builder);
    director.construct();
}

template <HumanSubRaceType H>
void HumanCRTPWithBuilder<H>::build (HumanoidWithRaceType& h) {
    const HumanBuilder<H> humanBuilder(h);
    const RacialClassDirector director(humanBuilder);
    director.construct();
}

template <ElfSubRaceType E>
void ElfCRTPWithBuilder<E>::build (HumanoidWithRaceType& h) {
    const ElfBuilder<E> elfBuilder(h);
    const RacialClassDirector director(elfBuilder);
    director.construct();
}

template <HighElfSubRaceType H>
void HighElfCRTPWithBuilder<H>::build (HumanoidWithRaceType& h) {
    const HighElfBuilder<H> highElfBuilder(h);
    const RacialClassDirector director(highElfBuilder);
    director.construct();
}

// Some totally different builder classes not related to race, but to be used sequentially with race builders.  Defined briefly only.
class PrimaryStatsBuilderBase : protected HumanoidWithRaceTypeBuilder {  // Builds based on level of the character class.
public:
    using HumanoidWithRaceTypeBuilder::HumanoidWithRaceTypeBuilder;
    void construct() const {  // Doing without the Director class this time, since here construct() also needs to be defined only once.
        establishHitPointsAndAbilityScores();
        establishProficiencies();
        establishSpells();
        // etc...
    }
private:
    virtual void establishHitPointsAndAbilityScores() const = 0;
    virtual void establishProficiencies() const = 0;
    virtual void establishSpells() const = 0;
    // etc...
};

class EquipmentBuilderBase : protected HumanoidWithRaceTypeBuilder {  // Builds based on the type of character class (and to some extent the race already established).
public:
    using HumanoidWithRaceTypeBuilder::HumanoidWithRaceTypeBuilder;
    void construct() const {  // Doing without the Director class this time, since here construct() also needs to be defined only once.
        establishWeapons();
        establishArmor();
        // etc...
    }
private:
    virtual void establishWeapons() const = 0;
    virtual void establishArmor() const = 0;
    // etc...
};

enum CharacteClassType { FIGHTER, WIZARD, CLERIC };  // etc...

class CharacterClass : public HumanoidWithRaceType {
    int level;
public:
    using HumanoidWithRaceType::HumanoidWithRaceType;
    void setLevel (int n) { level = n; }
    virtual void print (std::ostream& os = std::cout) const override {
        HumanoidWithRaceType::print(os);
        os << "Level: " << level << '\n';
    }
    virtual void save (std::ostream& os) const override {
        HumanoidWithRaceType::save(os);
        os << level << '\n';
    }
    virtual void load (std::istream& is) override {
        HumanoidWithRaceType::load(is);
        is >> level;
    }
};

template <CharacteClassType> class PrimaryStatsBuilder;

template <>
class PrimaryStatsBuilder<FIGHTER> : public PrimaryStatsBuilderBase {
public:
    using PrimaryStatsBuilderBase::PrimaryStatsBuilderBase;
private:
    virtual void establishHitPointsAndAbilityScores() const override {
        const int level = 5;
        dynamic_cast<CharacterClass&>(getBeing()).setLevel(level);
        getBeing().setHitPoints(level*10);
        // etc...
    }
    virtual void establishProficiencies() const override { std::cout << "\nProficiency added: can bend bars.\n"; }
    virtual void establishSpells() const override { std::cout << "\nNo spells allowed for Fighters.\n"; }
};
// Similar specializations for PrimaryStatsBuilder<WIZARD>, PrimaryStatsBuilder<CLERIC>, etc...

template <CharacteClassType> class EquipmentBuilder;

template <>
class EquipmentBuilder<FIGHTER> : public EquipmentBuilderBase {
public:
    using EquipmentBuilderBase::EquipmentBuilderBase;
private:
    virtual void establishWeapons() const override {  // These overrides are related to race due to racial sizes.
        if (getBeing().getRace()->isElf())
            getBeing().setWeapon(new ShortSword);
        else
            getBeing().setWeapon(new LongSword);
    }
    virtual void establishArmor() const override {
        if (getBeing().getRace()->isElf())
            getBeing().setArmor(new SmallChainMail);
        else
            getBeing().setArmor(new LargeChainMail);
    }
};
// Similar specializations for EquipmentBuilder<WIZARD>, EquipmentBuilder<CLERIC>, etc...

template <CharacteClassType C>
class CharacterClassCRTP : public CharacterClass {
protected:
    using CharacterClass::CharacterClass;
    void intializeWithBuilders() {
        const PrimaryStatsBuilder<C> primaryStatsBuilder(*this);
        primaryStatsBuilder.construct();
        const EquipmentBuilder<C> equipmentBuilder(*this);  // This can be used because race has already been established.
        equipmentBuilder.construct();
    }
};

template <CharacteClassType> class Character;

template <>
class Character<FIGHTER> : public CharacterClassCRTP<FIGHTER> {  // Fighter
public:
    Character (Race* race) : CharacterClassCRTP<FIGHTER>(race) {
        intializeWithBuilders();  // ***** All three separate builder classes are used in this constructor.
    }
    Character() = default;
};
template <> class Character<WIZARD> : public CharacterClassCRTP<WIZARD> { };  // Wizard (is similar to Fighter)
template <> class Character<CLERIC> : public CharacterClassCRTP<CLERIC> { };  // Cleric (is similar to Fighter)
using Fighter = Character<FIGHTER>;

int main() {
    HumanoidWithRaceType humanoid1;
    humanoid1.setRace(Human::getInstance());
    std::cout << "\nAfter being built with assigned race Human:\n";
    humanoid1.print();
    humanoid1.setRace(HumanSubRace<CHONDATHAN>::getInstance());
    std::cout << "\nAfter being built with assigned race Chondathan Human (sub-race of Human, but nothing special about it):\n";;
    humanoid1.print();  
    
    HumanoidWithRaceType humanoid2;
    humanoid2.setRace(HumanSubRace<CALASHITE>::getInstance());
    std::cout << "\nAfter being built with assigned race Calashite Human (sub-race of Human):\n";
    humanoid2.print();
    
    HumanoidWithRaceType humanoid3;
    humanoid3.setRace(Elf::getInstance());
    std::cout << "\nAfter being built with assigned race Elf:\n";
    humanoid3.print();
        
    HumanoidWithRaceType humanoid4;
    humanoid4.setRace(ElfSubRace<HIGH>::getInstance());
    std::cout << "\nAfter being built with assigned race High Elf (sub-race of Elf):\n";
    humanoid4.print();

    std::cout << "\n\nGenerating a Fighter now:\n";
    const std::string raceNameGenerated = "Grey Elf";  // From some Factory method.
    Fighter fighter(RaceDatabase::getRaceFromString(raceNameGenerated));  // For Factory methods, race must be passed into the Fighter constructor to use EquipmentBuilder due to race requiring to be known first.
    std::cout << "\nFighter, after being built with assigned race Grey Elf (sub-race of HighElf):\n";
    fighter.print();
    
    const std::string fileName = "SaveFile.txt";
    std::ofstream outfile(fileName);
    fighter.save(outfile);
    std::cout << "\nGrey Elf fighter written to txt file.\n";
    outfile.close();
    std::ifstream infile(fileName);
    
    Fighter loadedFighter;
    loadedFighter.load(infile);
    std::cout << "\nGrey Elf fighter constructed from txt file:\n";
    loadedFighter.print();
}

Output:

{Human, 0x1959d0} inserted in RaceDatabase::map.
{Calashite Human, 0x195a50} inserted in RaceDatabase::map.
{Chondathan Human, 0x195ad0} inserted in RaceDatabase::map.
{Damaran Human, 0x195d40} inserted in RaceDatabase::map.
{Elf, 0x195cd0} inserted in RaceDatabase::map.
{High Elf, 0x195d50} inserted in RaceDatabase::map.
{Grey Elf, 0x195ca0} inserted in RaceDatabase::map.
{Valley Elf, 0x195be0} inserted in RaceDatabase::map.

After being built with assigned race Human:
Intelligence: 9
Wisdom: 9
Dexterity: 9
Height: 180
Hit Points: 10
No weapon in hand.
No armor worn.
Proficiency With missiles = 5
Race: Human

After being built with assigned race Chondathan Human (sub-race of Human, but nothing special about it):
Intelligence: 9
Wisdom: 9
Dexterity: 9
Height: 180
Hit Points: 10
No weapon in hand.
No armor worn.
Proficiency With missiles = 5
Race: Chondathan Human

After being built with assigned race Calashite Human (sub-race of Human):
Intelligence: 11
Wisdom: 9
Dexterity: 9
Height: 187
Hit Points: 10
No weapon in hand.
No armor worn.
Proficiency With missiles = 6
Race: Calashite Human

After being built with assigned race Elf:
Intelligence: 9
Wisdom: 9
Dexterity: 11
Height: 150
Hit Points: 10
No weapon in hand.
No armor worn.
Proficiency With missiles = 5
Race: Elf

After being built with assigned race High Elf (sub-race of Elf):
Intelligence: 10
Wisdom: 9
Dexterity: 11
Height: 150
Hit Points: 10
No weapon in hand.
No armor worn.
Proficiency With missiles = 7
Race: High Elf


Generating a Fighter now:

Proficiency added: can bend bars.

No spells allowed for Fighters.

Fighter, after being built with assigned race Grey Elf (sub-race of HighElf):
Intelligence: 10
Wisdom: 10
Dexterity: 11
Height: 140
Hit Points: 50
Holding short sword as a weapon.
Wearing small chainmail for armor.
Proficiency With missiles = 8
Race: Grey Elf
Level: 5

Grey Elf fighter written to txt file.

Grey Elf fighter constructed from txt file:
Intelligence: 10
Wisdom: 10
Dexterity: 11
Height: 140
Hit Points: 50
Holding short sword as a weapon.
Wearing small chainmail for armor.
Proficiency With missiles = 8
Race: Grey Elf
Level: 5
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Code style

Your code style is very compact, making it hard to read in places, and it's not consistent. I strongly recommend that you don't write more than one statement per line.

Note that it's OK to be a bit sloppy while writing the code, however you should make use of your code editor's built-in code formatting features, or use an external tool like Artistic Style or ClangFormat once in a while.

Memory leaks

You use new in your code but there are no corresponding delete statements, thus your code leaks memory. Instead of using raw pointers, consider using smart pointers like std::unique_ptr together with helper functions like std::make_unique().

Your CRTP classes are not CRTPs

The curiously recurring template pattern looks like this:

template<typename T>
class CRTP {
    …
};

class Foo: CRTP<Foo> {
    …
};

Note how the template paramter of the CRTP class is exactly the same as the type that is inheriting from it. However, in your code you have this pattern:

template<typename T>
class YourCRTP {
    …
};

template<typename T>
class Foo: YourCRTP<T> {
    …
};

Here your "CRTP" class's template parameter is not Foo<T> but just T, so there is no curious recurring going on.

Class hierarchy

It's quite weird to see part of a character's properties being handled by inheritance, and part of it by having a Race * member variable. I would use inheritance for every aspect of a character where you can say it "is a". So a wild elf is a woodland elf is an elf is a humanoid is a demihuman is a living being. You'd use a pointer member variable where you would say "has a", and you would not say that an humanoid has a wild elf.

You should also consider whether it is appropriate to encode all this using the C++ type system. Consider the character classes like fighter, wizard and thief. In AD&D, while most characters would have the same character class from start to finish, it is possible that this changes during their life. Consider dual-class characters, which start out with one class and then gain another class later. Class changes are also not unheard off. So instead of using inheritance or pointers to base classes, you could just have the type stored as a member variable by value:

template<typename Race>
class Character: Race {
    CharacterClassType character_class;
    …
};

Is the builder pattern necessary here?

I fail to see why you need the builder pattern. In fact, your example code never actually calls build(). This makes me think the YAGNI principle applies here.

Unnecessary use of classes

Unlike Java, not everything in C++ needs to be in a class. Consider RaceDatabase, where everything is static. You could just make free functions and put them into a namespace:

namespace RaceDatabase {

std::unordered_map<std::string, Race *> map;

Race *fromString(const std::string &raceName) {
    return map.at(raceName);
}
…

}

Avoid polluting the global namespace

Putting something with a very generic name like NOT_FOUND into the global namespace will quickly become a problem in larger projects where it risks conflicting with other parts of the code. So it's always a good idea to move things out of the global namespace if possible. You can explicitly create a namespace to do that, but often you can also move types and static variables into related classes. If that is not an option, then at least make sure the names are less generic.

NOT_FOUND has apparently nothing to do with files not being found, but rather to indicate a race doesn't have infravision. So this can be moved into class Race, and could also be renamed to something more appropriate. You can also move the enum for the race type into class Race:

class Race {
public:
    enum Type {HUMAN, ELF, DWARF, HALFLING};
    constexpr int NO_DARK_VISION = -1;
    …
    virtual int getDarkVisionDistance() const {
        return NO_DARK_VISION;
    }
    …
};

Of course, this works for Race, but you can't move HumanSubRaceType into class HumanSubRace.

Scalability

The goal of having a class hierarchy is so you have base classes that take care of common things, and derived classes that specialize on top of that. This reduces the amount of code you have to maintain. However, consider your class Race: it has a member function isElf(). That is a bad idea, because what if you want to know if an object that derived from Race is a dwarf? Or a woodland elf? Are you going to add isDwarf() and isWoodlandElf()? This will get out of hand really fast. Instead of these isSomething() functions, you can consider using [dynamic_cast()][6] to check if a pointer to a Race points to an Elf or ForgottenRealsWoodElf or any type you want.

There is also notAffectedBySleepSpell(). Why is this in Race to begin with? What if you have a LivingBeing that is not a HumanoidWithRaceType and you cast sleep on that being? What if you cast sleep on something that is not a living being? What about the myriad of other spells you can cast?

Note that the AD&D books often have tables showing the properties of the various races, classes and so on. Considering this, maybe encoding these tables into your program and making it more data-driven is a better approach.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.