Random Number Generation

This lesson covers the basics of using randomness, with practical applications
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, Unlimited Access
3D art showing a character playing with dice
Ryan McCombe
Ryan McCombe
Updated

The ability to simulate randomness plays a surprisingly big role in creating software. In the context of graphics, for example, we often use randomness to simulate natural phenomena, like wind and other random effects.

In a role-playing game, it can be used to implement things like:

  • Loot Drops
  • Damage Ranges
  • Dodge Chances

We'll implement the last two of these in this lesson, in the context of a simple combat system.

We'll be able to write code like this, where our Character does between 15 and 25 damage on each attack, and has a 20% chance to dodge incoming damage:

class Character {
  // Each attack can do between 15 and 25 damage
  int MinDamage { 15 };
  int MaxDamage { 25 };

  // Character has a 20% chance to avoid damage
  float DodgeChance { 0.2 };

  int Health { 100 };

  void TakeDamage(int Damage) {
    if (Random::Bool(DodgeChance)) {
      return;
    }
    Health -= Damage;
  }

  void Act(Character* Target) {
    Target->TakeDamage(
      Random::Int(MinDamage, MaxDamage)
    );
  }
};

The new concepts here, and the key to getting this working, are these Random::Int and Random::Bool functions.

We'll create these functions in a file called Random.h that we can include in our other files as needed. We will also wrap our code in a namespace:

// Random.h
#pragma once

namespace Random {
  // Our code here
}

Generating a Random Number

Generating our random numbers involves 3 steps:

  1. A seeder, that will create an initial "random" number for us
  2. An engine, that will use the initial seed value to efficiently generate an endless supply of more random numbers
  3. A distributor that will convert these random numbers into the desired range, such as from 15 to 25 in our combat example

Let's get started!

Seeding a Random Number Generator

The process of getting a random number from a computer is not as simple as it might seem. Computers are, by design, deterministic machines.

Instead, how we simulate randomness is to source some unpredictable data, and then perform mathematical operations to convert that data to an unpredictable number.

This initial unpredictable input is called the seed. In this lesson, we will simply ask the operating system to provide this seed.

To get access to this, we need to #include the <random> header from the standard library. We can then create a variable of type std::random_device.

// Random.h
#pragma once
#include <random>

namespace Random {
  using namespace std;
  random_device seeder;
}

How does the operating system generate the seed?

Operating systems have built-in techniques to generate random numbers.

How they do this varies, but typically, they combine data from a variety of places. Common sources include things like:

  • Timing data, like the time of the last 100 events of a specific type
  • User input, like the pattern of recent mouse movement
  • Arbitrary sensor data, like temperatures and ambient sound

These are commonly referred to as sources of entropy, and the operating system combines many of these sources to create a number that is so unpredictable it is effectively "random".

Random Number Engines

The process of generating a seed is quite resource-intensive, so it is not something we want to do frequently.

Instead, the best practice is to take that seed and pass it into a random number engine that can quickly generate new numbers.

A common mathematical algorithm for doing this is called the Mersenne Twister

An implementation of this is also available in the standard library, under std::mt19937. We can initialize our engine using this algorithm, passing it a seed value generated from our seeder.

// Random.h
#pragma once
#include <random>

namespace Random {
  using namespace std;
  random_device seeder;
  mt19937 engine { seeder() };
}

Calling an Object?

In the above example, the expression seeder() may seem weird. seeder is an object of type std::random_device, but we appear to be calling it as if it were a function.

Based on what we've learned so far, we'd expect to call a function on an object, but not call an object directly.

The key to understanding what is happening here is that () is an operator in C++. That means it can be overloaded within a class, and that's exactly what the developers who created std::random_device did.

This lets us create objects that have all the power of their classes but can also be used as a function. Such an object is sometimes called a functor. We cover functors in more detail, including how to create our own, in the next course.

Random Number Distributions

Typically, when we are using randomness, we want it to be constrained to a certain range of possibilities.

For example, we want a damage value to be somewhere between 15 and 25.

To do this, we need to remap the possible outputs from our random number engine to the values within this range.

The standard library has std::uniform_int_distribution which can help us here.

To use it, we create an object of this type, calling the constructor with two integers - the lower and upper limit of the range. Below, we’ve called our object get. To generate a random number in this range, we then "call" get, passing in our engine:

std::uniform_int_distribution get { 1, 10 };

// Get a random integer from 1 to 10
get(engine);

Let's see all our components working together:

// Random.h
#pragma once
#include <random>

namespace Random {
  using namespace std;
  random_device seeder;
  mt19937 engine{seeder()};

  int Int(int min, int max) {
    uniform_int_distribution get{min, max};
    return get(engine);
  }
}

Now, within any of our other classes, we can import this header file, and start generating random integers. We just need to call Random::Int with a lower and upper limit. Below, we log out three random numbers from 1-10:

// main.cpp
#include <iostream>
#include "Random.h"

int main() {
  std::cout << Random::Int(1, 10) << ", "
    << Random::Int(1, 10) << ", "
    << Random::Int(1, 10);
}
5, 1, 7

Below, we use it in our Character class to make Act inflict a random amount of damage based on the MinDamage and MaxDamage class variables:

// Character.h
#pragma once
#include "Random.h"

class Character {
  // Each attack can do between 15 and 25 damage
  int MinDamage { 15 };
  int MaxDamage { 25 };
  int Health{100};

  void TakeDamage(int Damage) {
    Health -= Damage;
  }

  void Act(Character* Target) {
    Target->TakeDamage(
      Random::Int(MinDamage, MaxDamage)
    );
  }
};

Uniform and Normal Distributions

Here, we're using a uniform distribution, but other options are available. In a uniform distribution, each number in the range is equally likely to be chosen. In this example where our range is 15-25, a uniform distribution means that 15, 17, and 19 are going to be equally common results.

Another common strategy is to use a normal distribution. In a normal distribution, the closer a number is to the center of the range, the more frequently it will be chosen. In this example, that means 15 will rarely occur, 17 will be more common, and 19 will be chosen frequently.

If we choose 100 random integers from 15-25, the following chart shows how frequently we might expect each number to be chosen, depending on our choice of distribution:

Two charts showing a normal and uniform distribution

Random Booleans

A typical requirement involves causing an event to happen a specific proportion of the time. For example, maybe a character dodges 20% of attacks or performs a critical strike 10% of the time.

It would be useful to be able to encapsulate this within our Random namespace.

Let's use a different distributor function this time - std::uniform_real_distribution distributes our random numbers to a range of possible floats.

By distributing from 0.0 to 1.0, and then comparing it to our desired probability, we can return true that proportion of the time.

Let's make this available as Random::Bool():

#pragma once
#include <random>

namespace Random {
  using namespace std;
  random_device seeder;
  mt19937 engine { seeder() };

  int Int(int min, int max) {
	uniform_int_distribution get { min, max };
	return get(engine);
  }

  bool Bool(float probability) {
	  uniform_real_distribution get { 0.0, 1.0 };
	  return probability > get(engine);
  }
}

Let's see it in action:

// main.cpp
#include <iostream>
#include "Random.h"

int main() {
  for (int i{0}; i < 8; ++i) {
    if (Random::Bool(0.2)) {
      std::cout << "Dodge, ";
    } else {
      std::cout << "Hit, ";
    }
  }
}
Hit, Hit, Dodge, Hit, Hit, Hit, Hit, Dodge,

And let's add it to our class:

// Character.h
#pragma once
#include "Random.h"

class Character {
  // Each attack can do between 15 and 25 damage
  int MinDamage { 15 };
  int MaxDamage { 25 };

  // Character has a 20% chance to avoid damage
  float DodgeChance { 0.2 };

  int Health{100};

  void TakeDamage(int Damage) {
    if (Random::Bool(DodgeChance)) {
      return; // Dodged!  No damage
    }
    Health -= Damage;
  }

  void Act(Character* Target) {
    Target->TakeDamage(
      Random::Int(MinDamage, MaxDamage)
    );
  }
};

Controlling and Reproducing Randomness

When working with randomness, we often also want some visibility or control of what is happening. For example, it can be difficult to reproduce a bug, or replay a sequence of events, if we have no control over the seed of our random number generator.

Often, we may even want to give users control over the seed. This is common in strategy games for example, where the map might be randomly generated, but we want users to be able to provide the seed.

This allows them to replay a map they enjoyed, or share it with their friends. Without being able to control the seed, that is impossible.

Let's begin by updating our code to store the seed we use as a variable, so we can see what it is. By using auto deduction or checking the documentation we see that random_device uses an unsigned integer.

We'll also add a PrintSeed function, so we can see what value was chosen.

// Random.h
#pragma once
#include <iostream>
#include <random>

namespace Random {
  using namespace std;
  random_device seeder;
  unsigned int seed { seeder() };
  mt19937 engine { seed };

  void PrintSeed() {
    cout << "Seed: " << seed << '\n';
  }

  int Int(int min, int max) {
    uniform_int_distribution get { min, max };
    return get(engine);
  }
}

And let's use it:

#include <iostream>
#include "Random.h"

int main() {
    using namespace std;
    Random::PrintSeed();
    cout << Random::Int(1, 100) << ", "
         << Random::Int(1, 100) << ", "
         << Random::Int(1, 100);
}

Running this code yields output similar to:

Seed: 141102604
60, 78, 47

Of course, running the code a second time would generate a different output. But, now that we know the seed, we could use that to get the same results again, if that is what we (or our user) wanted.

Let's add a Reseed method to our Random namespace, which can be called any time we want to reproduce the same results:

// Random.h
#pragma once
#include <iostream>
#include <random>

namespace Random {
  using namespace std;
  random_device seeder;
  unsigned int seed { seeder() };
  mt19937 engine { seed };

  void PrintSeed() {
    cout << "Seed: " << seed << '\n';
  }

  void Reseed(unsigned int NewSeed) {
    seed = NewSeed;
    engine.seed(NewSeed);
  }

  int Int(int min, int max) {
    uniform_int_distribution get { min, max };
    return get(engine);
  }
}

Finally, let's make use of it in our code, to let us exert some control over our randomness:

#include <iostream>
#include "Random.h"

int main() {
  using std::cout;
  // We can still get random numbers:
  cout << Random::Int(1, 100) << ", "
       << Random::Int(1, 100) << ", "
       << Random::Int(1, 100) << "\n\n";

  // But now we can create reproducible results:
  Random::Reseed(141102604);
  Random::PrintSeed();
  cout << Random::Int(1, 100) << ", "
       << Random::Int(1, 100) << ", "
       << Random::Int(1, 100) << "\n\n";

  cout << "Let's go again - ";
  Random::Reseed(141102604);
  Random::PrintSeed();
  cout << Random::Int(1, 100) << ", "
       << Random::Int(1, 100) << ", "
       << Random::Int(1, 100) << "\n\n";
}

Our output is this:

19, 4, 97

Seed: 141102604
60, 78, 47

Let's go again - Seed: 141102604
60, 78, 47

Summary

In this lesson, we explored the fundamentals of implementing randomness in C++ by enhancing a simple combat system. We delved into the concepts of random number generators, distributions, and the practical application of these concepts in a game-like scenario. The key topics we learned included:

  • Understanding the role of randomness in software.
  • Learning to implement random number generation in C++, including the use of std::random_device, std::mt19937 (Mersenne Twister), and different types of distributions.
  • Developing Random::Int and Random::Bool functions to generate random integers and booleans within specified ranges or probabilities.
  • Grasping the importance of seeding in random number generation and how operating systems generate seeds from various sources of entropy.
  • Gaining insights into controlling and reproducing randomness, including the use of seeds for debugging and creating repeatable scenarios in our programs.

Preview of the Next Lesson

In our next lesson, we will explore how to handle dates and times in C++. We'll investigate the chrono library, std::time_t, and the tm struct, all integral components for manipulating and representing time in various ways. The key topics we’ll cover include:

  • Introduction to the chrono library and its significance in modern C++ programming.
  • Understanding std::time_t for representing time as a duration since the epoch.
  • Exploring the tm struct for detailed time representation.
  • Converting between different time formats using C++ functions.
  • Practical examples demonstrating how to manipulate and format dates and times.

Was this lesson useful?

Next Lesson

Dates, Times and Durations

Learn the basics of the chrono library and discover how to effectively manage durations, clocks, and time points
3D art showing a sundial in a fantasy environment
Ryan McCombe
Ryan McCombe
Updated
3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, Unlimited Access
3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, unlimited access

This course includes:

  • 56 Lessons
  • Over 200 Quiz Questions
  • 95% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Dates, Times and Durations

Learn the basics of the chrono library and discover how to effectively manage durations, clocks, and time points
3D art showing a sundial in a fantasy environment
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved