The foundation of Make It Random is the concept of a random engine, an object providing the basic services of a pseudo-random number generator (PRNG), responsible for generating a deterministic sequence of seemingly random bits initialized by a seed value.
This topic contains the following sections:
All random engine classes implement IBitGenerator and IRandom. The core of every random engine is the pair of functions Next32 and Next64. These two functions will generate 32 or 64 bits of random data, based on the engine's current state, and will update that state so that the next call to either of these functions will generate a new collection of 32 or 64 bits.
Aside from the caveats that PRNGs are entirely deterministic, and that some PRNGs are better at hiding the dependence of later values on earlier values, all bits should seem to be entirely independent of all other bits, either bits from earliers calls or the same call to Next32 or Next64. True and false bit values should occur with exactly equal probability.
All other random data produced by the extension methods of Make It Random are generated entirely through the data returned by these two methods.
PRNGs typically support the concept of a Seed, a value that is used to initialize the state of the random engine. If a particular type of PRNG is initialized with a particular seed value, it is guaranteed to produce exactly the same sequence of bits every time. Seeds can thus be incredibly valuable when game behavior is needed that appears random, but can reproduced on demand (such as in a replay).
Random engines in Make It Random all share a uniform interface for specifying seeds, despite often having substantially different internal state structures. This is accomplished through the help of the RandomStateGenerator class. This class can accept seed data input in a variety of formats, and will then hash that data to produce a sequence of bits, similar to random engines themselves, which can be used to initialized the internal state of a random engine.
IRandom provides a few different overloads of the Seed function to reseed a random engine, taking a few common values as input. And in case none of those types are sufficient, an already-initialized instance of RandomStateGenerator, another instance of IRandom, or any other object implementing IBitGenerator can be supplied as input to reseed the engine. Concrete random engine types such as XorShift128Plus provide a similar set of static Create(...) functions to specify the seed value directly when creating a new instance of a random engine.
The Seed functions completely replace the prior state of the random engine. If instead you want the new state to be influenced by both the current state and the value of a new seed, you can use one of the MergeSeed functions instead.
Whenever a parameterless seed-related or creation function is called, unpredictable seed data is generated by the default constructor RandomStateGenerator. This data includes variable inputs such as the current time, the amount of time that has passed since the system was started, and the process id, as well as a global value which is altered every time this constructor is called. This helps to ensure that a random engine does not by default produce the exact same sequence of values every time the program is run, and likewise that multiple independent random engines created in quick succession of each other do not all produce the same sequence of values. If you instead want such cases to always produce the same sequence, be sure to use an explicit seed value, even if it is nothing more than an arbitrarily chosen hard-coded integer.
At any given moment, an instance of a random engine will have an internal state value which entirely determines what values the following calls to Next32 or Next64 will produce. Different types of random engines will have different internal structures for this state, but at the very least, this state can always be represented as an opaque byte array.
As such, IRandom provides the methods SaveState and RestoreState(Byte), allowing you to easily serialize and deserialize random engines. This is similar to using seeds, but is more powerful in that it can capture and restore a random engine's state at any point in a random engine's deterministic sequence, whereas seeds can only be used to restore a random engine's state at the beginning of some particular sequence.
Concrete implementations of IRandom may also include overloads of SaveState(...) and RestoreState(...), as well as CreateWithState(...) functions, that expose the internal data structure of the random engine, avoiding the need to work with opaque byte arrays when the specific type of the random engine is known and fixed.
Do not mistake any concrete implementation's of Create(...) that takes a byte array or a parameter that matches the implementations internal state data structure as a creation function that will restore that state. These functions always take seed values, and are free to do any deterministic hashing that they want on the seeds before assigning the results to the internal state. If you have explicit state data, always ensure that you use CreateWithState(...) to create and initialize a random engine with that state.
Some random engines are mathematically structured such that there is an efficient way to skip over a vast number of values in the deterministic sequence without having to make that same number of explicitly calls to Next32 or Next64 (or the related function Step which updates the internal state but returns no value). Instead, they update the step in a way that is equivalent to making all those calls, but typically has a time complexity of O(log n) where n is the number of skipped values.
A common use case is in parallel computation, where one wishes to use just a single seed to initialize a random engine, and then to split the usage of that random engine across multiple threads, processes, or machines. In this case, each parallel execution can take a copy of the originally seeded engine and jump ahead a different number of intervals from any other parallel execution. If the intervals are large enough, this guarantees that each parallel execution will be pulling from an entirely different section of the overall sequence of random bits, and will never run long enough to overlap with any other parallel execution.
The XorShift128+ engine is both fast and small while still exhibiting high quality randomness, and is the recommended engine to use in most cases.
The full version of Make It Random includes the following additional random engines:
Although a static interface or a singleton instance of a random engine is a less flexible design than the instantiable model used by Make It Random, it can nonetheless be convenient at times, especially for quick experiments and prototypes. Make It Random therefore provides such a global instance through the static property shared.
This instance is guaranteed to be created upon use, and unless explicitly changed, will be an instance returned by a call to CreateStandard, which will currently produce an instance of XorShift128Plus, seeded with unpredictable data from a default-initialized instance of RandomStateGenerator.
If XorShift128Plus does not fit your needs and you wish to make your own random engine, simply implement IRandom and all the advanced functionality provided by Make It Random extension methods will immediately apply.
And although it is not necessary, it is prudent to also provide custom implementations for the following:
Note that some methods not in the first list above have a default implementation that throws NotSupportedException if you do not provide an implementation of your own.