Skip to Content
Course content

235: The <random> Header: Distributions and Engines

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

If you've spent any time in older C++ codebases, you've probably seen std::rand() and the modulo operator (%) used to constrain numbers. Honestly, stop doing that. std::rand() has poor statistical properties and the modulo approach introduces "modulo bias," making some numbers appear more often than others. Modern C++ gives us the <random> header, which separates the generation of raw random bits from the shaping of those bits into a useful range or pattern.

Picking the Engine

Think of the "engine" as the source of entropy. It's a stateful object that just spits out a sequence of raw, unbiased numbers. For almost every practical purpose, you want std::mt19937. It's a Mersenne Twister—fast, has a massive period before it repeats, and is far more reliable than the old C-style generators.

To get this started, we need a seed. We don't want the same "random" sequence every time the program runs, so we use std::random_device to grab a truly random seed from the hardware.

#include <iostream>
#include <random>
#include <string>
#include <vector>

int main() {
    // The seed source
    std::random_device rd; 
    // The engine, initialized with the seed
    std::mt19937 gen(rd()); 
    
    return 0;
}

Defining the Loot Table with Uniform Distributions

Now that we have a source of randomness, we need a "distribution." This is the part that takes the raw output of the engine and maps it to something we actually care about. Since we're building a simple game loot system, we'll start with a std::uniform_int_distribution to pick an item from a list.

std::vector<std::string> items = {"Rusty Sword", "Healing Potion", "Wooden Shield", "Iron Ore"};

// We want a random index between 0 and the last element of our vector
std::uniform_int_distribution<int> dist(0, items.size() - 1);

// To get a number, we pass the engine into the distribution object
int index = dist(gen);
std::cout << "You found a: " << items[index] << std::endl;

The "Same Number" Trap

Here is where I usually trip up when I'm rushing a prototype. I might decide to wrap the loot logic into a helper function to keep main() clean. I'll do something like this:

// DANGER: Don't do this
int rollLoot() {
    std::random_device rd;
    std::mt19937 gen(rd()); 
    std::uniform_int_distribution<int> dist(0, 3);
    return dist(gen);
}

If you call this function in a tight loop, you might notice the "random" numbers start looking very suspicious, or your performance tanks. Why? Because I'm re-seeding and re-instantiating the entire engine every single time the function is called. The engine is meant to be a long-lived object. You seed it once, and you keep using it.

The fix is simple: make the engine static so it's initialized only once for the lifetime of the program, or pass it by reference into the function. I prefer static for simple utility functions.

int rollLoot() {
    static std::random_device rd;
    static std::mt19937 gen(rd()); 
    std::uniform_int_distribution<int> dist(0, 3);
    return dist(gen);
}

Adding Variance with Normal Distributions

Uniform distributions are great for "one of these," but real-world data (and game balance) often follows a bell curve. Let's say each item has a "Quality Score." Most items should be average, a few should be terrible, and a few should be legendary. This is exactly what std::normal_distribution is for.

It takes a mean (the center of the bell) and a standard deviation (how spread out the curve is).

// Mean of 50, standard deviation of 10
std::normal_distribution<double> qualityDist(50.0, 10.0);

double quality = qualityDist(gen);
std::cout << "Item Quality: " << quality << std::endl;

Now, if we combine these, we have a system that can pick a random item and then assign it a realistically varied quality score. The engine generates the raw noise, the uniform_int_distribution picks the item, and the normal_distribution determines the quality.




📋 Practical Task

Build a Combat Critical-Hit Generator

Create a program that simulates 10 attacks of a character. For each attack, you need to implement the following logic using the <random> header:

  • Critical Chance: Use a std::bernoulli_distribution (which returns a boolean) with a probability of 0.15 (15%) to determine if the hit is a "Critical Hit."
  • Damage Calculation:
    • If it is a normal hit, use a std::uniform_int_distribution to generate damage between 10 and 20.
    • If it is a critical hit, use a std::normal_distribution to generate damage with a mean of 40 and a standard deviation of 5.

Ensure your std::mt19937 engine is initialized only once (hint: use static or declare it in main and pass it to a function) to avoid the re-seeding mistake discussed in the lesson. Print the result of each attack (e.g., "Normal Hit: 14 damage" or "CRITICAL HIT: 42.3 damage").

Rating
0 0

There are no comments for now.

to be the first to leave a comment.