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.