I've been working on building a video game in my spare time, and because I've played tabletop board games for most of my life, (and for the last 6 years, tabletop role-playing games), I am partial to using integer dice as a method of randomization. Thinking about implementation of randomization, I was considering using the classic rand( ) calls from the C Standard Library <stdlib.h>. I of course then remembered that in order to use rand( ), you had to seed the generator first with srand( unsigned int ), and then of course you need to apply the basic formula for bounding the result with ( rand( ) % ( max - ( min + 1 ) ) ) + 1. This is all very basic stuff covered in every introductory C course.
The big problem I had with calling rand( ) at all is that an srand( ) call seeds all number generation in the thread on Windows and in the process on POSIX-compliant systems[1]. This means that on non-Windows systems, no matter how many threads I had with instances of the die class, they would all draw from the same random number pool. The other problem that I had with this was that using the old rand( ) formula actually favors lower numbers and is not nearly as close to the ideal of a uniform distribution because of how modulo division works[2]. In order to correct this without having to write code to handle doing Box-Muller transforms[3], I decided to use the C++11 <random> library.
My initial setup for the die class was as follows:
die.h 1|
#ifndef _DIE_H
2|
#define _DIE_H
3| 4|
#include <random>
5| 6|
class
Die
{
7|
public
:
8| Die
( );
9|
virtual
~
Die
( );
10| 11|
int
roll
(
int
bottom,
int
top
);
12|
protected
:
13|
private
:
14|
};
15| 16|
#endif
// _DIE_H
The code that I had governing the generation of a random number came directly from a tutorial on cplusplus.com[4]. On the site it gives an example of declaring a default_random_engine and then using it with a uniform_int_distribution to generate random numbers. I was not paying attention to the fact that default_random_engine was a class and forgot to initialize it with a seed, giving the Die class the behavior of always outputting the bottom value given as an argument. After quickly realizing my mistake, I needed to make some adjustments to my code and then clean it up.die.c 1|
#include "die.h"
2| 3| Die
::
Die
() {
4| 5|
}
6| 7| Die
::~
Die
() {
8| 9|
}
10| 11|
int
Die
::
roll
(
int
bottom
,
int
top
) {
12|
if
(
bottom
>
top
) {
13|
return
0
;
14|
}
else if
(
bottom
==
top
) {
15|
return
top
;
16|
}
else
{
17|
std::default_random_engine
rng
;
18|
std::uniform_int_distribution
<
int
>
range
(
bottom
,
top
);
19|
return
range
(
rng
);
20|
}
21|
}
First thing to do was to generate a seed to randomize the output of the die for each instance. Using the <chrono> library from C++11, I was able to get the system time as follows:
Ideally I should be using a random_device to generate the numbers for the uniform_int_distribution but I'm not sure of all of the implementation details of it. Usually it attempts to open /dev/urandom (or use the Intel RDRND instruction)[5] but I couldn't find any information on how to ensure it's easily compilable for all system configurations and it did not implicitly work on my machine. As such I've decided to leave playing with it for another time and I'll probably make a post about my experience when I get to it.1|
unsigned
seed
=
std
::
chrono
::
system_clock
::
now
().
time_since_epoch
().
count
();
2|
std::default_random_engine
rng
(
seed
);
The next thing to do was to remove as much redundant code as possible, which for me meant moving the number generator constructor up into the initialization function so that it did not need to be initialized each time. On top of that I set up a function to allow the user to get a new seed without having to call new and reconstruct the object and I overloaded the roll function so that the user could create a uniform int distribution and reuse it with the same Die.
After setting all these changes together, my code looked like:
die.h 1|
#ifndef _DIE_H
2|
#define _DIE_H
3| 4|
#include <chrono>
5|
#include <random>
6| 7|
class
Die
{
8|
public
:
9| Die
( );
10|
virtual
~
Die
( );
11| 12|
int
roll
(
int
bottom,
int
top
);
13|
int
roll
(
int
bottom,
int
top
);
14| void reseed( ); 15|
protected
:
16|
private
:
17|
std::default_random_engine
rng
;
18|
};
19| 20|
#endif
// _DIE_H
die.c 1|
#include "die.h"
2| 3| Die
::
Die
() {
4| reseed( ); 5|
}
6| 7| Die
::~
Die
() {
8|
}
9| 10|
int
Die
::
roll
(
int
bottom
,
int
top
) {
11|
if
(
bottom
>
top
) {
12|
return
0
;
13|
}
else if
(
bottom
==
top
) {
14|
return
top
;
15|
}
else
{
16|
std::uniform_int_distribution
<
int
>
range
(
bottom
,
top
);
17|
return
range
(
rng
);
18|
}
19|
}
20| 21|
int
Die
::
roll
(
std::uniform_int_distribution
<
int
> * range
) {
22| return
(*
range
)(rng);
23| }
24|
25|
void
Die::reseed( ) {
26|
delete
(&
rng
);
27|
unsigned
seed =
std
::
chrono
::
system_clock
::
now
().
time_since_epoch
().
count
()
;
28|
new (&rng) std::default_random_engine( seed )
;
29|
}
Running a program to test this class produced the desired results. This class is largely an abstraction of the default_random_engine class, but the reason I made it was to make the code in my game neat and clear.
References
[2] What's wrong with rand( ) % N ?[3] Box-Muller Transform
[4] <random> - C++ Reference
[5] std::random_device::random_device - cppreference.com