#052 – Crash course on implementing a Mersenne Twister

051 – Introduction to random number generation in C++ covered the theory of pseudo-random number generation in C++ and ended with stating that the Mersenne Twister is the most plausible engine for non-cryptographic work in native C++.

This Nibble shows what that looks like in practice: how I integrated std::mt19937 into the combat system of a turn-based RPG to drive critical hits, dodges, and damage variance.

The Context

In my RPG, combatEngine encapsulates the gameplay loop of fighting. Combat in the game has several rolls per attack: a dodge check, a critical-hit check, and a damage-variance multiplier.

class CombatEngine {
public:
    CombatEngine();

    AttackResult CalculateAttack(
        const StatBlock& attackerStats,
        const StatBlock& defenderStats,
        const DamageStrategy& strategy,
        int baseDamage);

    bool  RollCritical(int critChance);
    bool  RollDodge(int dodgeChance);
    float RollDamageVariance(float variancePercent = 0.1f);

private:
    mutable std::mt19937 m_RandomEngine;

    std::uniform_int_distribution<int>    m_PercentRoll{ 1, 100 };
    std::uniform_real_distribution<float> m_VarianceRoll{ 0.9f, 1.1f };

    // 
}

NOTE: m_RandomEngine is declared mutable so that const member functions on CombatEngine can still draw from it

1. Seed in the constructor

To guarantee unique numbers on every program execution, we seed via std::random_device. This asks the OS for a pseudo-random number.

CombatEngine::CombatEngine()
    : m_RandomEngine(std::random_device{}()) { }

2. Molding it

mt19937 generates 32-bit unsigned integers. We need to shape this for the game. We want to simulate the critical hit chance (0 – 100%) and damage variance to make combat exciting (0.9 – 1.1).

For this, we can use a distribution. Think of this as molding the PRNG.

std::uniform_int_distribution<int>    m_PercentRoll{ 1, 100 };
std::uniform_real_distribution<float> m_VarianceRoll{ 0.9f, 1.1f };

When we want to calculate the critical hit chance, we pass the Mersenne Twister into the distribution

bool CombatEngine::RollCritical(int critChance) {
    critChance = std::clamp(critChance, 0, 100);
    return m_PercentRoll(m_RandomEngine) <= critChance; // Mold the PRNG
}

For damage variance:

float CombatEngine::RollDamageVariance(float variancePercent) {
    std::uniform_real_distribution<float> dist(
        1.0f - variancePercent,
        1.0f + variancePercent);
    return dist(m_RandomEngine);
}

3. Using it

AttackResult CombatEngine::CalculateAttack(
    const StatBlock& attackerStats,
    const StatBlock& defenderStats,
    const DamageStrategy& strategy,
    int baseDamage)
{
    AttackResult result{};

    if (RollDodge(CalculateDodgeChance(defenderStats))) {
        result.wasDodged = true;
        return result;
    }

    int damage = strategy.CalculateDamage(baseDamage, attackerStats);
    
    // Invoke the Mersenne Twister
    float variance = RollDamageVariance(); 
    damage = static_cast<int>(damage * variance);
    // ...

In Conclusion

C++ offers the Mersenne Twister for easy, reliable and mediocre random number generation. An application of its used in a C++ RPG was covered.