Skip to Content
Course content

202: Random Number Generation with <random>

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this exact snippet of code in countless junior PRs and student projects. It looks innocent enough, but it's a disaster in production. Let's look at a function designed to simulate a critical hit in a game:

#include <iostream>
#include <cstdlib>
#include <ctime>

bool isCriticalHit() {
    std::srand(std::time(nullptr)); // Seed the generator
    return (std::rand() % 100) < 10; // 10% chance
}

int main() {
    for (int i = 0; i < 5; ++i) {
        std::cout << (isCriticalHit() ? "CRIT! " : "Miss ") <;
    }
    return 0;
}

If you run this, you'll notice something weird. Instead of a random sequence, you'll likely get the exact same result five times in a row (e.g., "Miss Miss Miss Miss Miss"). It feels broken because it is broken.

The problem with seeding inside a loop

The issue here is that std::time(nullptr) returns the current time in seconds. Because modern CPUs execute that loop in a tiny fraction of a second, std::srand() is being called with the exact same seed value every single time the function runs. In the world of pseudo-random number generation, if you provide the same seed, you get the exact same sequence of numbers. You're essentially resetting the "random" sequence back to the start every time you check for a crit.

Even if you moved the seed to main(), std::rand() is generally frowned upon in modern C++. It often has poor distribution (the lower bits are less random than the higher bits) and a very small maximum range (RAND_MAX can be as low as 32,767 on some platforms), which makes it useless for high-precision simulations.

Switching to the Mersenne Twister

To do this properly, we need the <random> header. Instead of a global state that we seed with srand, we create a generator object. The industry standard for most general-purpose work is std::mt19937, which is a Mersenne Twister engine. It has a massive period, meaning it takes a very long time before the sequence repeats.

Here is how I would rewrite that critical hit logic:

#include <iostream>
#include <random>

bool isCriticalHit() {
    // 'static' ensures the engine is initialized only once for the life of the program
    static std::random_device rd; 
    static std::mt19937 gen(rd()); 
    
    // We define the range here: 0 to 99
    std::uniform_int_distribution<int> dist(0, 99);
    
    return dist(gen) < 10;
}

Notice the static keyword. This is crucial. I don't want to re-seed the engine every time the function is called (which brings us back to the original bug), but I also don't want to pass a generator object through every single function call in my game engine. By making it static, the engine lives for the duration of the program, and we just sample from it.

Decoupling Generation from Distribution

One of the biggest shifts in <random> compared to the old rand() % N approach is the separation of the engine and the distribution. The engine (mt19937) just spits out a raw stream of huge, random numbers. The distribution (uniform_int_distribution) takes that raw stream and shapes it into the range and type you actually need.

Why bother with this complexity? Because rand() % 100 suffers from "modulo bias." If RAND_MAX isn't perfectly divisible by 100, some numbers will appear slightly more often than others. While that doesn't matter for a casual game, it's a nightmare for cryptography or scientific simulations. Using a distribution object handles the math for you, ensuring every number in your range has an mathematically equal chance of appearing.

Handling Floating Point Ranges

If you need a random decimal—say, for a damage multiplier between 1.5x and 2.5x—you don't do the weird casting gymnastics required by rand(). You just swap the distribution type:

std::uniform_real_distribution<double> damageDist(1.5, 2.5);
double multiplier = damageDist(gen);

It's clean, explicit, and doesn't rely on some hidden global state that might be modified by another library you've included. Just remember: seed your engine once, and use distributions to shape the output.




📋 Practical Task

Exercise: Implementing a Weighted Loot Table

In many RPGs, loot isn't perfectly uniform. A "Common" item might have a 70% drop rate, "Rare" 20%, and "Legendary" 10%.

Your task is to create a program that simulates a loot drop. Use the <random> library to implement the following logic:

  • Create a std::mt19937 generator seeded with std::random_device.
  • Use a std::uniform_int_distribution<int> to generate a number between 1 and 100.
  • Based on the result, print the rarity of the item:
    • 1-70: "Common Item"
    • 71-90: "Rare Item"
    • 91-100: "Legendary Item!"
  • Wrap this in a loop that simulates 10 separate loot drops to verify the distribution feels correct.

Challenge: Try making the generator static inside a function called std::string rollLoot() so that the function can be called multiple times without re-seeding.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.