I require a PRNG which:
- Has repeatable results from a given state.
- Is both seeded from and has a state containing a sufficiently large amount of data (somewhere in the region of 700 bits or more) to cover every possible permutation of a fairly large Fisher-Yates shuffle.
- Can be run on .NET.
A few generators exist which satisfy the first two conditions, but finding one that also satisfies the third has proven more difficult, so I decided to port one myself. I chose to adapt xoroshiro1024**[Public Domain] and have (as far as I can tell) successfully converted the original C code (or at least as much of it as is required) to C#.
using System;
using System.Linq;
using System.Numerics;
using System.Security.Cryptography;
namespace CoreGamePlugin.Utilities.Random;
public class XoRoShiRo1024StarStar
{
private byte position;
private readonly ulong[] state;
public XoRoShiRo1024StarStar()
{
state = new ulong[16];
byte[] seedBytes;
do { seedBytes = RandomNumberGenerator.GetBytes(129); }
while (seedBytes[..^1].All(x => x == byte.MinValue));
for (byte i = 0; i < 16; i++)
{
state[i] = BitConverter.ToUInt64(seedBytes, i * 8);
}
position = (byte)(seedBytes[128] % 16);
}
public ulong NextULong()
{
byte previousPosition = position;
ulong activeSection = state[position = (byte)(++position % 16)];
ulong newSection = state[previousPosition];
newSection ^= activeSection;
state[previousPosition] = BitOperations.RotateLeft(activeSection, 25) ^ newSection ^ (newSection << 27);
state[position] = BitOperations.RotateLeft(newSection, 36);
return BitOperations.RotateLeft(activeSection * 5, 7) * 9;
}
// Further methods utilising the result of NextULong()...
}
The problem lies in the fact that I do not know C, and cannot claim to fully understand the nuances of C or what every keyword actually did in the original code. As such, while my version of the code runs, appears to pass the tests I have performed for distribution uniformity, and mirrors the behaviour of the original for a handful of seeds over a short period (but longer than two full cycles of the state
's position
, i.e. more than 32 calls to NextULong()
), I am still concerned there may exist an edge case for which this may not hold true.
Additionally, I am unsure if my method for seeding the generator is appropriate, so any advice with regards to that would be very helpful. In particular, is there a risk of the RandomNumberGenerator
"running out of entropy" and slowing/breaking the application if too many instances of the generator are created in quick succession, and how might this be addressed? In case it is relevant: this code will be running in various different environments/OS, including both locally on users' machines and on at least one server, with the latter being far more likely to require many instances of the generator at once.
Edit: Having thought more thoroughly about the cyclical nature of this PRNG's state
while responding to a comment, I have come to the realisation—perhaps rather embarrassingly late—that seeding the position
is entirely superfluous (as two instances of this generator would produce the same output if the second had both its state
rotated by some number of places and the position
accordingly increased or decreased by the same value [assuming correct adherance to the modulo operation] compared to the first), and so in my code it is now initialised outside of the constructor as so: private byte position = byte.MinValue;
, along with the relevant modifications this would imply for the constructor itself. However, as per the rules of this site, I have left the code above as it was at the time of first answer.
BitOperations.RotateLeft
(orulong
instead ofuint64_t
), that is an obvious and necessary part of the translation. Some of the other changes make it harder to tell whether this code is a correct port or not, since there is no nice 1-to-1 correspondence between the original code and the port. \$\endgroup\$