TripleX - leveraging functions, maps and RAII patterns

I’ve used C++ professionally for years and love to study it. Here’s what I came up for TripleX:

Code
#include <iostream>
#include <ctime>
#include <map>
#include <vector>
#include <string>

#include <functional>
#include <algorithm>

using std::cin;
using std::cout;
using std::endl;
using std::map;
using std::vector;
using std::string;
using std::function;

// making it an enum class will prevent us from doing errors like writing Play(3) or Play(false) instead of Play(EASIEST)
enum class EDifficulty {
    EASIEST,
    EASIER,
    EASY,
    STANDARD,
    INTERESTING,
    CHALLENGING,
    HARD,
    HARDER,
    EXTREME,
    NIGHTMARISH,
    IMPOSSIBLE
};

// this order is important for the progression.
static const vector<EDifficulty> Difficulties = {
    EDifficulty::EASIEST,
    EDifficulty::EASIER,
    EDifficulty::EASY,
    EDifficulty::STANDARD,
    EDifficulty::INTERESTING,
    EDifficulty::CHALLENGING,
    EDifficulty::HARD,
    EDifficulty::HARDER,
    EDifficulty::EXTREME,
    EDifficulty::NIGHTMARISH,
    EDifficulty::IMPOSSIBLE
};

static const map<EDifficulty, unsigned short int> DifficultyValue = {
    { EDifficulty::EASIEST,      2},
    { EDifficulty::EASIER,       3},
    { EDifficulty::EASY,         4},
    { EDifficulty::STANDARD,     5},
    { EDifficulty::INTERESTING,  6},
    { EDifficulty::CHALLENGING,  7},
    { EDifficulty::HARD,         8},
    { EDifficulty::HARDER,       9},
    { EDifficulty::EXTREME,      10},
    // the latter difficulties will have additional rules to make them more difficult, let's not bump numbers any higher.
    { EDifficulty::NIGHTMARISH,  10},
    { EDifficulty::IMPOSSIBLE,   10}
};

static const map<EDifficulty, string> DifficultyDescription = {
    { EDifficulty::EASIEST,      "a tiny snake"},
    { EDifficulty::EASIER,       "a small flying imp"},
    { EDifficulty::EASY,         "an angered imp"},
    { EDifficulty::STANDARD,     "a cruel goblin"},
    { EDifficulty::INTERESTING,  "a bloodthirsty orc"},
    { EDifficulty::CHALLENGING,  "a dreadful dark-armored knight"},
    { EDifficulty::HARD,         "a giant, intimidating spider"},
    { EDifficulty::HARDER,       "a hive of killer bees, seeping with murderous intent"},
    { EDifficulty::EXTREME,      "a sphinx, consumed by time"},
    { EDifficulty::NIGHTMARISH,  "an abominable entity with physical features that continuously change"},
    { EDifficulty::IMPOSSIBLE,   "the deepest void of the world, where no light and no sound can ever reach"}
};

enum class EAdditionalRules {
    ALL_ODD,
    ALL_DIFFERENT
};

static bool ValidInput(unsigned int a, unsigned int b, unsigned int c)
{
    return a != 0
        && b != 0
        && c != 0;
}

typedef function<void(int&, int&, int&)> ValidityChecker;

static ValidityChecker CheckAscending = [](int& a, int& b, int& c) {
    // just sort them in ascending order and reassign them.
    std::vector<int> temp = { a, b, c };
    std::sort(temp.begin(), temp.end());
    a = temp[0]; b = temp[1]; c = temp[2];
};

static ValidityChecker CheckAllDifferent = [](int& a, int& b, int& c) {
    if (a == b
        || b == c
        || a == c)
        // at least one of them is equal: this selection is invalid.
        a = b = c = 0;
};

static ValidityChecker CheckOdd = [](int& a, int& b, int& c) {
    if    ((a % 2 == 0)
        || (b % 2 == 0)
        || (c % 2 == 0))
        // at least one of them is even: this selection is invalid.
        a = b = c = 0;
};

static const map <EAdditionalRules, ValidityChecker> RuleImplementation =
{
    { EAdditionalRules::ALL_ODD, CheckOdd },
    { EAdditionalRules::ALL_DIFFERENT, CheckAllDifferent }
};

static const map <EAdditionalRules, string> RuleDescription =
{
    { EAdditionalRules::ALL_ODD, "The three numbers must all be odd numbers." },
    { EAdditionalRules::ALL_DIFFERENT, "The three numbers must all be different from each other." }
};

static const map<EDifficulty, vector<EAdditionalRules>> AdditionalRulesForDifficulty = {
    { EDifficulty::NIGHTMARISH, {EAdditionalRules::ALL_ODD}},
    { EDifficulty::IMPOSSIBLE, {EAdditionalRules::ALL_ODD, EAdditionalRules::ALL_DIFFERENT}}
};

static string SCENERY_DESCRIPTION(EDifficulty difficulty)
{
    return string("After walking for a while, you find in front of you ") + DifficultyDescription.at(difficulty) + ". A question is posed to you: if you don't answer right, your journey will be over.";
}

static string Introduction = "You're the Nameless Hero, sent to quell an unknown evil. You've received the task with honor, and proudly started walking towards the path that the elders have indicated.",
    GameOver = "You can feel the entity's malice overpowering you, and before you know anything your senses dim and your consciousness fades. Will the next Nameless Hero save the world instead of you?",
    Win = "The entity shrinks and fades, terrified by your knowledge and wisdom. You can safely journey onwards.",
    GameEnd = "At last, you find the unknown evil. It is... a simple lever, near a small waterfall, gushing out clean water on mud.\r\n"
        "What a waste, you think. As you switch the lever, the water stops flowing.\r\n"
        "Will the gods ever learn to close the water stream when they're done with the dishes?",
    Question = "You will have to give three numbers: ", QuestionSum = "their sum is ", QuestionProduct = "; their product is ", QuestionEnter = "Write them now, separated by a space and followed by an 'x'.";

class InputGatherer
{
public:
    InputGatherer()
    {
        int stop;
        cin >> a >> b >> c >> stop;
        CheckAscending(a, b, c);
    }

    bool matches(int questionA, int questionB, int questionC)
    {
        return a == questionA
            && b == questionB
            && c == questionC;
    }

    // use a RAII approach to clear buffer. When this object is destroyed, cin will be reset.
    ~InputGatherer()
    {
        cin.clear();
        cin.ignore();
    }

private:
    int a, b, c;
};

bool PlayerWinsDifficulty(EDifficulty difficulty)
{
    int difficultyLevel = DifficultyValue.at(difficulty);

    // three question numbers
    int questionA, questionB, questionC;

    // let's retrieve our question validity checkers
    vector<ValidityChecker> validators;
    vector<EAdditionalRules> additionalRules;
    if (AdditionalRulesForDifficulty.find(difficulty) != AdditionalRulesForDifficulty.end())
        additionalRules = AdditionalRulesForDifficulty.at(difficulty);

    for (auto rule : additionalRules)
        validators.push_back(RuleImplementation.at(rule));

    // generate different numbers until they satisfy the conditions. Unless some validator invalidate them, they will always be valid.
    do {
        questionA = (rand() * 100) % difficultyLevel + difficultyLevel;
        questionB = (rand() * 100) % difficultyLevel + difficultyLevel;
        questionC = (rand() * 100) % difficultyLevel + difficultyLevel;

        for (auto validator : validators)
        {
            validator(questionA, questionB, questionC);
        }
    } while (!ValidInput(questionA, questionB, questionC));

    // this is to make the player win even if he gives them in a different order.
    CheckAscending(questionA, questionB, questionC);

    cout << SCENERY_DESCRIPTION(difficulty) << endl;
    cout << Question << QuestionSum << (questionA + questionB + questionC) << QuestionProduct << (questionA * questionB * questionC) << "." << endl;
    for (auto rule : additionalRules)
        cout << RuleDescription.at(rule) << endl;

    cout << endl << QuestionEnter << endl;

    auto input = InputGatherer();
    return input.matches(questionA, questionB, questionC);
}

int main()
{
    cout << Introduction << endl;

    for (auto difficulty : Difficulties)
    {
        if (!PlayerWinsDifficulty(difficulty))
        {
            cout << GameOver << endl;;
            return 0;
        }
        else
        {
            cout << Win << endl;
        }
    }

    cout << GameEnd << endl;

    return 0;
}

I wanted to focus on clean code practices, while keeping a fun game. I also took the occasion to fix the “you lost but you may continue” behaviour and customize the difficulties a bit.
Enjoy a standard fantasy journey as it gets darker and darker… with a final twist!

3 Likes

Oh wow! @Aaron_Stackpole Look at this TripleX!

Amazing work man! This is incredible! I can’t wait to see what you do in the next section with Bull Cow Game!

1 Like

Interesting approach, quite clean, reminds me a little bit of Dan’s approach in some ways, definitely conforms to Unreal standards better than I do! :wink: I particularly like how additional rules are implemented to make the game more challenging as the levels go up!

1 Like

Yeah I agree. I think the additional rules are unique and creates a overall better player experience as the game goes on.

Thank you, even though I realized afterwards that the additional rules don’t actually make it harder as the game goes on - if anything, they provide additional hints :smiley: however the structure would work if I came up with real challenges. I thought about time limits but it would have been a little too different from the original concept :slight_smile:

Sometimes different is better :wink:

Privacy & Terms