Thursday, June 16, 2016

Generating Random Integers in C++

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  
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|  }  
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.

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:
  1|  unsigned seed = std::chrono::system_clock::now().time_since_epoch().count();  
  2|  std::default_random_engine rng( seed );  
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.

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

No comments:

Post a Comment