Neon Royale

RNG in Slots: How the Random Number Generator Works — PRNG vs TRNG, Certification

Neon AdminNeon Admin·Mar 12, 2026
RNG in Slots: How the Random Number Generator Works — PRNG vs TRNG, Certification

Introduction

Every spin of a slot machine begins with a single event that the player never sees: the generation of a random number. That number — or more precisely, a sequence of numbers — determines which symbols land on the reels, whether a bonus triggers, and ultimately how much the player wins or loses.

The Random Number Generator is not a detail. It is the foundation on which everything else is built. Get it wrong and the entire mathematical model collapses: RTP drifts, combinations cluster in patterns, and the game becomes exploitable. Get it right and it becomes invisible — an inviolable source of entropy that neither the player, nor the operator, nor the developer can predict or manipulate.

For a slot developer, RNG sits at the intersection of mathematics, cryptography, software engineering, and regulatory compliance. This article covers all four dimensions: what randomness actually means in a computational context, how PRNG and TRNG differ and why it matters, how to implement a compliant RNG in C#, how certification laboratories test it, and what Provably Fair systems look like.

By the end of this article you will understand not just how to implement an RNG, but why each design decision matters — from the choice of algorithm to the seeding strategy to the statistical tests your implementation must pass before it can go live.


Part I. The Nature of Randomness in Computing

1.1 True Randomness Does Not Exist in a Deterministic Machine

A computer, at its core, is a deterministic machine. Given the same input, it produces the same output — every time, without exception. This creates a fundamental paradox: how do you generate something genuinely unpredictable on a machine that is, by design, perfectly predictable?

The answer is: you can't generate true randomness purely in software. What you can do is generate sequences of numbers that are statistically indistinguishable from random, using algorithms so complex that predicting the next output is computationally infeasible — even if you know the algorithm.

This is the basis of the Pseudorandom Number Generator (PRNG).

1.2 What Makes a Sequence "Random Enough"?

In the context of slot machines, a sequence of numbers is considered sufficiently random if it satisfies all of the following properties:

Uniformity — every possible output value appears with equal frequency over a long run. For a generator producing numbers in the range [0, N), each value should appear approximately N/total_outputs times.

Independence — knowing the previous output tells you nothing about the next one. There are no detectable correlations between consecutive values.

Unpredictability — given any number of past outputs, it is computationally infeasible to predict any future output. This is the cryptographic requirement, and it is the hardest to satisfy.

Long period — the sequence must not repeat within any timeframe relevant to the game. A generator with a period of 2³² (~4 billion) would cycle through all its states in roughly 4 hours at 1,000 spins per second — unacceptably short.

Seed sensitivity — a tiny change in the initial seed should produce a completely different output sequence. This prevents attackers from narrowing down the seed space through trial and error.

1.3 Why Randomness Matters Beyond Fairness

It might seem that "good enough" randomness is just about fairness to the player. In reality, a weak RNG creates vulnerabilities on multiple fronts:

Exploit attacks. In 2014, a group of attackers reverse-engineered the PRNG used by an Australian slot manufacturer. By observing the timing and outcomes of a small number of spins, they were able to predict future outcomes with sufficient accuracy to bet only on winning spins. The manufacturer lost millions before the exploit was discovered. A cryptographically secure RNG makes this attack computationally infeasible.

Pattern detection. Human beings are extraordinarily good at pattern recognition — often seeing patterns that don't exist. If a PRNG has any detectable periodicity or clustering, players will notice it, report it, and regulators will investigate. Even a statistically valid but perceptually patterned sequence creates reputational and regulatory risk.

Audit failure. Certification laboratories run battery after battery of statistical tests. A PRNG that fails even one test cannot be certified, and an uncertified game cannot go live in any regulated jurisdiction.


Part II. PRNG — Pseudorandom Number Generators

2.1 How a PRNG Works

A PRNG is a deterministic algorithm that takes an initial value called a seed and produces a long sequence of numbers that appear random. The core operation is a state transition function:

state(n+1) = f(state(n))
output(n)  = g(state(n))

Where f is the transition function (updates internal state) and g is the output function (extracts a number from the current state).

The entire sequence is deterministically determined by the initial seed. Given the same seed, a PRNG always produces the same sequence — which is simultaneously its greatest strength (reproducibility for auditing) and its greatest weakness (predictability if the seed is known or guessable).

2.2 The Mersenne Twister: The Most Famous PRNG

Mersenne Twister (MT19937), developed by Matsumoto and Nishimura in 1997, was for many years the de facto standard PRNG in many fields including gambling software. Understanding it is essential context, even if you won't use it directly.

Key properties:

Period: 2^19937 − 1 (astronomically long — will never cycle in practice)

Passes virtually all classical statistical tests (Diehard, TestU01)

Extremely fast: generates ~100M numbers per second on modern hardware

State size: 624 × 32-bit integers = 2496 bytes

How it works (simplified):

State: array of 624 32-bit integers w[0..623]

Initialization:
  w[0] = seed
  w[i] = 1812433253 × (w[i-1] XOR (w[i-1] >> 30)) + i  (for i=1..623)

Generation (twist operation every 624 numbers):
  for i in 0..623:
    y = (w[i] & 0x80000000) | (w[(i+1) % 624] & 0x7FFFFFFF)
    w[i] = w[(i+397) % 624] XOR (y >> 1)
    if y is odd: w[i] = w[i] XOR 2567483615

Output (tempering):
  y = w[index]
  y = y XOR (y >> 11)
  y = y XOR ((y << 7) & 2636928640)
  y = y XOR ((y << 15) & 4022730752)
  y = y XOR (y >> 18)
  return y

Why MT19937 is NOT suitable for cryptographic use (and therefore not for slots):

The Mersenne Twister's critical flaw is that its internal state can be fully reconstructed from 624 consecutive outputs. An attacker who observes 624 outputs knows the complete state and can predict all future outputs.

For a slot machine that shows its results to the player, this is a catastrophic weakness. In practice, reconstructing the state takes a few seconds of computation on a modern laptop. A player who plays 624 spins in a row — easily done in an hour of normal play — has, in principle, enough information to predict every subsequent spin.

NEVER use Mersenne Twister (or any non-cryptographic PRNG) as the primary RNG in a real-money iGaming product.

2.3 Other Common PRNGs (and Their Limitations)

Linear Congruential Generator (LCG):

X(n+1) = (a × X(n) + c) mod m

Simple and fast. Used in early slot machines and in System.Random in older .NET. Extremely weak — the entire state is a single integer, and the output is trivially predictable. Never appropriate for gambling.

Xoshiro256 / Xorshift: Modern, extremely fast PRNGs with excellent statistical properties and long periods. Widely used in game engines, simulations, and non-security applications. Still not cryptographically secure — state recovery attacks exist. Not appropriate as the sole RNG for real-money games.

WELL (Well Equidistributed Long-period Linear): An improvement over Mersenne Twister in terms of initialization speed and statistical properties. Still not cryptographically secure. Same verdict.

ChaCha20: This is a different category entirely — a stream cipher used as a CSPRNG. Cryptographically secure, fast, and used in modern TLS. This is the kind of algorithm we want.

2.4 The CSPRNG: Cryptographically Secure PRNG

A CSPRNG (Cryptographically Secure PRNG) is a PRNG that satisfies additional requirements beyond statistical randomness:

Next-bit unpredictability: Given any sequence of k output bits, no polynomial-time algorithm can predict the (k+1)th bit with probability greater than 0.5 + ε (for negligible ε).

State compromise recovery: Even if the internal state is compromised at time T, past outputs (before T) remain computationally secure.

These properties make the CSPRNG suitable for cryptographic operations — and by extension, for any application where predictability would cause real harm. Real-money gambling is explicitly such an application.

CSPRNGs suitable for iGaming:

Algorithm

Basis

Speed

Used In

ChaCha20

Stream cipher

Very fast

Modern TLS, OS entropy

AES-CTR

Block cipher

Fast (with AES-NI)

NIST SP 800-90A

HMAC-DRBG

HMAC + SHA-256/512

Moderate

NIST SP 800-90A, FIPS 140

Hash-DRBG

SHA-2 family

Moderate

NIST SP 800-90A

OS CSPRNG

Platform entropy + algo

Fast

/dev/urandom, BCryptGenRandom

In C#/.NET, the correct tool is System.Security.Cryptography.RandomNumberGenerator — a CSPRNG backed by the operating system's entropy pool (/dev/urandom on Linux, BCryptGenRandom on Windows, SecRandomCopyBytes on macOS).


Part III. TRNG — True Random Number Generators

3.1 What Is a TRNG?

A TRNG (True Random Number Generator) derives randomness from physical processes that are inherently non-deterministic at a quantum or thermodynamic level:

Thermal noise (Johnson-Nyquist noise in resistors)

Radioactive decay (timing between decay events)

Photon shot noise (quantum uncertainty in optical systems)

Atmospheric noise (random.org uses this)

CPU jitter (timing variations in CPU instruction execution)

The key distinction from PRNG: a TRNG's output is genuinely non-deterministic. There is no seed. There is no algorithm that could, in principle, predict the next output even with unlimited computational resources.

3.2 TRNG in Hardware

Modern CPUs include hardware TRNGs on-chip:

Intel RDRAND / RDSEED:

Available on Intel Ivy Bridge (2012) and later

RDRAND: cryptographically processed random from an entropy source. Produces up to 800 MB/s

RDSEED: raw entropy (higher quality, lower throughput)

Both accessible from C# via interop or via .NET's RandomNumberGenerator (which uses them automatically on supported hardware)

AMD equivalent:

AMD processors from Zen architecture (2017+) include equivalent hardware entropy sources

ARM TrustZone:

Hardware RNG in modern ARM SoCs, used in mobile devices

Dedicated hardware RNG cards:

Used in high-security environments: Entropy Key, IDQ Quantis (quantum RNG), Comscire PQ4000

Quantum RNG devices (like IDQ Quantis) derive entropy from quantum vacuum fluctuations — genuinely unpredictable at a fundamental physics level

3.3 TRNG in Practice: The Limitation

TRNGs have one significant practical limitation: throughput. A hardware TRNG may generate only a few kilobytes of truly random data per second. A busy game server handling thousands of simultaneous spins needs millions of random numbers per second — far beyond what a TRNG can supply.

The standard solution is the hybrid model:

TRNG  ──[entropy]──▶  CSPRNG seed/reseed  ──[output]──▶  Game Server
                      (refreshed periodically)

The TRNG continuously feeds entropy into the CSPRNG, periodically reseeding it with fresh true randomness. The game server consumes output from the CSPRNG. This gives you:

The statistical and throughput properties of a well-designed CSPRNG

Unpredictability anchored in genuine physical entropy

Resistance to state compromise (periodic reseeding)

This hybrid model is what modern operating system CSPRNGs implement (/dev/urandom, BCryptGenRandom). When you call RandomNumberGenerator.GetBytes() in C#, this is what happens under the hood.

3.4 PRNG vs TRNG: The Practical Decision for iGaming

Property

PRNG (non-cryptographic)

CSPRNG

TRNG

Hybrid (TRNG + CSPRNG)

Truly non-deterministic

✅ (effectively)

Cryptographically secure

High throughput

Reproducible (for audit)

Partially

Partially

GLI/BMM certifiable

Suitable for real-money slots

The practical recommendation: use your OS CSPRNG (RandomNumberGenerator in C#) as your primary source. It already implements the hybrid model internally. For maximum auditability, layer a documented CSPRNG algorithm (HMAC-DRBG or AES-CTR-DRBG per NIST SP 800-90A) on top of the OS entropy, so you can demonstrate to a certification lab exactly what algorithm you used and how it was seeded.


Part IV. RNG Implementation in C#

4.1 What NOT to Use

Before we get to correct implementations, let's be explicit about what must never appear in a real-money game server:

csharp

// ❌ NEVER use System.Random in a gambling context
var rng = new System.Random();
int result = rng.Next(0, 32);

// ❌ NEVER seed System.Random with a predictable value
var rng = new System.Random(DateTime.Now.Millisecond);

// ❌ NEVER use a static shared System.Random instance
// (thread-safety issues + sequential seed pattern)
private static readonly Random _shared = new Random();

// ❌ NEVER use Math.random() (JavaScript) for server-side logic
// (but you shouldn't have server logic in JS anyway)

System.Random in .NET uses a subtraction generator — a non-cryptographic PRNG that produces statistically weak output and is trivially predictable.

4.2 The Correct Approach: System.Security.Cryptography.RandomNumberGenerator

csharp

using System.Security.Cryptography;

public class CryptoRng : IDisposable
{
    // RandomNumberGenerator is thread-safe in .NET 6+
    // No need to instantiate per-request
    private static readonly RandomNumberGenerator _rng = 
        RandomNumberGenerator.Create();

    /// <summary>
    /// Returns a cryptographically secure random integer in [0, maxExclusive).
    /// Uses rejection sampling to eliminate modular bias.
    /// </summary>
    public static int NextInt(int maxExclusive)
    {
        if (maxExclusive <= 0)
            throw new ArgumentOutOfRangeException(nameof(maxExclusive));
        if (maxExclusive == 1)
            return 0;

        // Use .NET 6+ built-in which handles bias correctly
        return RandomNumberGenerator.GetInt32(maxExclusive);
    }

    /// <summary>
    /// Returns a cryptographically secure random integer in [minInclusive, maxExclusive).
    /// </summary>
    public static int NextInt(int minInclusive, int maxExclusive)
    {
        if (minInclusive >= maxExclusive)
            throw new ArgumentOutOfRangeException();

        return RandomNumberGenerator.GetInt32(minInclusive, maxExclusive);
    }

    /// <summary>
    /// Fills a buffer with cryptographically secure random bytes.
    /// </summary>
    public static void GetBytes(byte[] buffer)
        => RandomNumberGenerator.Fill(buffer);

    /// <summary>
    /// Returns a random double in [0.0, 1.0).
    /// </summary>
    public static double NextDouble()
    {
        Span<byte> bytes = stackalloc byte[8];
        RandomNumberGenerator.Fill(bytes);
        ulong value = BitConverter.ToUInt64(bytes);
        // Map to [0, 1) by dividing by 2^64
        return (value >> 11) * (1.0 / (1ul << 53));
    }

    public void Dispose() => _rng?.Dispose();
}

4.3 Why Rejection Sampling Matters

A common mistake when generating a random integer in a range is to use the modulo operator directly:

csharp

// ❌ BIASED — modulo introduces non-uniform distribution
int stop = (int)(GetRandomUInt32() % reelSize);

This introduces modular bias: if the range of the generator (2³² = 4,294,967,296) is not perfectly divisible by reelSize, some values will appear slightly more often than others.

Example: reelSize = 32. 4,294,967,296 / 32 = 134,217,728 exactly — no bias in this specific case. But with reelSize = 30: 4,294,967,296 / 30 = 143,165,576.53... The first 16 values appear one extra time, introducing a bias of ~0.0000023%. Tiny, but detectable by statistical tests and technically a certification failure.

The correct approach — rejection sampling — discards values that would introduce bias:

csharp

public static int UnbiasedInt(int maxExclusive)
{
    // .NET's RandomNumberGenerator.GetInt32() already does this correctly
    // Here is the manual implementation for educational purposes:
    
    uint threshold = (uint)((0x100000000UL % (ulong)maxExclusive));
    
    while (true)
    {
        Span<byte> bytes = stackalloc byte[4];
        RandomNumberGenerator.Fill(bytes);
        uint value = BitConverter.ToUInt32(bytes);
        
        if (value >= threshold)
            return (int)(value % (uint)maxExclusive);
        // If value < threshold, discard and retry
        // Expected retries: less than 1 per call in virtually all cases
    }
}

In practice: just use RandomNumberGenerator.GetInt32(maxExclusive) in .NET 6+. It handles this correctly internally. But you need to know why it does this, because certification labs will ask.

4.4 The SlotRng Class: Production-Ready Implementation

Here is a complete, production-ready RNG class for a slot game server:

csharp

using System.Security.Cryptography;
using System.Runtime.CompilerServices;

/// <summary>
/// Cryptographically secure RNG for iGaming use.
/// Thread-safe. Suitable for high-concurrency game servers.
/// Uses OS CSPRNG (BCryptGenRandom on Windows, /dev/urandom on Linux).
/// </summary>
public sealed class SlotRng : ISlotRng
{
    // In .NET 6+, RandomNumberGenerator is a static class with
    // thread-safe static methods. No instance management needed.

    /// <summary>
    /// Selects a random stop position on a reel.
    /// Returns an index in [0, reel.Length).
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public int NextReelStop(int reelSize)
    {
        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(reelSize);
        return RandomNumberGenerator.GetInt32(reelSize);
    }

    /// <summary>
    /// Generates stop positions for all reels in a single call.
    /// </summary>
    public int[] NextReelStops(IReadOnlyList<int> reelSizes)
    {
        var stops = new int[reelSizes.Count];
        for (int i = 0; i < reelSizes.Count; i++)
            stops[i] = RandomNumberGenerator.GetInt32(reelSizes[i]);
        return stops;
    }

    /// <summary>
    /// Selects a weighted random item.
    /// weights[i] is the relative probability of selecting item i.
    /// Uses a single RNG call for efficiency.
    /// </summary>
    public int NextWeighted(ReadOnlySpan<int> weights)
    {
        int total = 0;
        foreach (int w in weights) total += w;

        int roll = RandomNumberGenerator.GetInt32(total);

        int cumulative = 0;
        for (int i = 0; i < weights.Length; i++)
        {
            cumulative += weights[i];
            if (roll < cumulative)
                return i;
        }

        // Should never reach here if weights are valid
        throw new InvalidOperationException(
            "Weighted selection failed — check that all weights are non-negative.");
    }

    /// <summary>
    /// Generates a random seed for a game session.
    /// Returns a cryptographically secure 256-bit (32-byte) seed.
    /// </summary>
    public byte[] GenerateSessionSeed()
    {
        var seed = new byte[32];
        RandomNumberGenerator.Fill(seed);
        return seed;
    }

    /// <summary>
    /// Generates a server seed for Provably Fair verification.
    /// Returns the raw seed bytes and a SHA-256 hash commitment.
    /// </summary>
    public (byte[] Seed, string Commitment) GenerateProvablyFairSeed()
    {
        var seed = GenerateSessionSeed();
        var hash = SHA256.HashData(seed);
        return (seed, Convert.ToHexString(hash).ToLowerInvariant());
    }

    /// <summary>
    /// Deterministically produces a reel stop from a seed + nonce.
    /// Used for Provably Fair replay and audit.
    /// </summary>
    public int DeterministicReelStop(byte[] seed, long nonce, int reelIndex, int reelSize)
    {
        // Combine seed + nonce + reel index into a unique input
        Span<byte> input = stackalloc byte[seed.Length + 8 + 4];
        seed.CopyTo(input);
        BitConverter.TryWriteBytes(input[seed.Length..], nonce);
        BitConverter.TryWriteBytes(input[(seed.Length + 8)..], reelIndex);

        // HMAC-SHA256 produces 32 bytes of deterministic pseudorandom output
        var output = HMACSHA256.HashData(seed, input[seed.Length..]);

        // Extract a uint from the first 4 bytes and map to [0, reelSize)
        uint rawValue = BitConverter.ToUInt32(output);
        return UnbiasedReduce(rawValue, (uint)reelSize);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static int UnbiasedReduce(uint value, uint range)
    {
        // Daniel Lemire's fast unbiased reduction (2019)
        // Avoids division in the common case
        ulong m = (ulong)value * (ulong)range;
        uint  l = (uint)m;
        if (l < range)
        {
            uint threshold = (uint)(-(int)range % (int)range);
            while (l < threshold)
            {
                // Need a new random value — in deterministic mode this is a
                // protocol-level issue; for audit purposes this is acceptable
                // as the nonce must be incremented
                throw new InvalidOperationException(
                    "Bias threshold hit in deterministic mode — increment nonce.");
            }
        }
        return (int)(m >> 32);
    }
}

public interface ISlotRng
{
    int   NextReelStop(int reelSize);
    int[] NextReelStops(IReadOnlyList<int> reelSizes);
    int   NextWeighted(ReadOnlySpan<int> weights);
    byte[] GenerateSessionSeed();
    (byte[] Seed, string Commitment) GenerateProvablyFairSeed();
    int   DeterministicReelStop(byte[] seed, long nonce, int reelIndex, int reelSize);
}

4.5 Thread Safety Considerations

On a game server handling thousands of concurrent sessions, thread safety is not optional:

csharp

// ❌ WRONG — static System.Random is NOT thread-safe
private static readonly Random _rng = new Random();
// Concurrent access causes degraded randomness and potential exceptions

// ❌ WRONG — using a lock on System.Random creates a bottleneck
private static readonly object _lock = new();
private static readonly Random _rng = new Random();
int result = 0;
lock (_lock) { result = _rng.Next(32); }
// Correct but unnecessary — use the CSPRNG static methods instead

// ✅ CORRECT — RandomNumberGenerator static methods are thread-safe in .NET 6+
int result = RandomNumberGenerator.GetInt32(32);
// No lock needed. No shared mutable state. Safe for any concurrency level.

The static methods on RandomNumberGenerator in .NET 6+ use per-thread state internally where needed, making them both thread-safe and contention-free.


Part V. Seeding: The Most Critical Implementation Detail

5.1 Why the Seed Is Everything

A CSPRNG is only as secure as its seed. The best algorithm in the world is worthless if the seed is predictable.

Historical examples of seed-based attacks:

Timestamp seeding (classic vulnerability):

csharp

// ❌ CATASTROPHICALLY INSECURE — DO NOT USE
var rng = new Random((int)DateTime.Now.Ticks);

An attacker who knows approximately when the server started (e.g., from a log timestamp visible in error messages) can enumerate all possible seeds within a reasonable time window and predict all outputs.

Process ID seeding:

csharp

// ❌ INSECURE — process IDs are often predictable or observable
var rng = new Random(Environment.ProcessId);

Combining weak sources:

csharp

// ❌ STILL INSECURE — XOR of weak sources is still weak
int seed = Environment.TickCount ^ Environment.ProcessId ^ Thread.CurrentThread.ManagedThreadId;

The correct approach: derive seeds exclusively from the OS CSPRNG entropy pool:

csharp

// ✅ CORRECT — OS entropy cannot be predicted by an external observer
byte[] seedBytes = new byte[32];
RandomNumberGenerator.Fill(seedBytes);
// Use seedBytes to initialize any application-level state

5.2 Seeding Strategy for a Game Server

A production game server needs a clear seeding architecture:

System Entropy Pool (/dev/urandom or BCryptGenRandom)
         │
         ▼
OS CSPRNG  ←── hardware TRNG (if available, via RDRAND)
         │
         ▼  RandomNumberGenerator.Fill(bytes)
Application-Level Seeding
         │
         ├──▶ Session Seed (per-player-session, 256-bit)
         │         │
         │         ▼
         │    Session RNG state (HMAC-DRBG seeded from Session Seed)
         │         │
         │         ▼
         │    Spin outcomes (deterministic from Session Seed + spin nonce)
         │
         └──▶ Server Seed (per-game-instance, published after session ends)
                   │
                   ▼
              Provably Fair commitment (hash of Server Seed published upfront)

5.3 Reseeding Policy

Even a CSPRNG benefits from periodic reseeding with fresh entropy, especially on long-running servers:

csharp

public class ReseedingRng
{
    private readonly TimeSpan _reseedInterval = TimeSpan.FromMinutes(30);
    private DateTime _lastReseed = DateTime.UtcNow;
    private readonly object _reseedLock = new();

    // In practice, .NET's RandomNumberGenerator already does this
    // automatically via the OS entropy pool.
    // This class is for documentation/demonstration purposes
    // or for explicit audit trail requirements.

    private void ReseedIfNeeded()
    {
        if (DateTime.UtcNow - _lastReseed < _reseedInterval) return;

        lock (_reseedLock)
        {
            if (DateTime.UtcNow - _lastReseed < _reseedInterval) return;

            // Pull fresh entropy from OS
            byte[] freshEntropy = new byte[64];
            RandomNumberGenerator.Fill(freshEntropy);

            // Log the reseed event for audit purposes
            _auditLogger.LogReseedEvent(
                timestamp: DateTime.UtcNow,
                entropySource: "OS_CSPRNG",
                entropyBytes: freshEntropy.Length
            );

            _lastReseed = DateTime.UtcNow;
        }
    }
}

Part VI. Statistical Testing

6.1 Why Statistical Testing Is Required

Before a game can be certified, the RNG must pass a battery of statistical tests. These tests verify that the output distribution is indistinguishable from true randomness — no clustering, no periodicity, no systematic bias.

Certification laboratories (GLI, BMM, iTech Labs) run these tests on samples of at minimum 10 million numbers, typically much more.

6.2 The Test Batteries

NIST SP 800-22 (most widely used in iGaming certification)

The National Institute of Standards and Technology's test suite contains 15 tests:

Test

What It Checks

Frequency (Monobit)

Equal proportion of 0s and 1s in the entire sequence

Block Frequency

Equal proportion of 0s and 1s in blocks of M bits

Runs

Number and length of "runs" (uninterrupted sequences of identical bits)

Longest Run of Ones

Distribution of the longest run of 1s in 128-bit blocks

Binary Matrix Rank

Linear independence of rows in binary matrices from the sequence

Discrete Fourier Transform

Absence of periodic patterns (spectral analysis)

Non-Overlapping Template Matching

Frequency of specific non-overlapping patterns

Overlapping Template Matching

Frequency of specific overlapping patterns

Maurer's Universal

Compressibility (a compressible sequence is not random)

Linear Complexity

Length of the shortest LFSR that produces the sequence

Serial

Frequency of overlapping m-bit patterns

Approximate Entropy

Comparison of frequencies of overlapping m vs m+1 bit patterns

Cumulative Sums

Sum of transformed output should stay near zero

Random Excursions

Number of cycles in a random walk

Random Excursions Variant

Visits to specific states in a random walk

Diehard Battery (Marsaglia, 1995)

An older but respected battery of 15 tests including Birthday Spacings, Overlapping Permutations, Parking Lot, Squeeze, and others. Still required by some certification labs.

TestU01 (L'Ecuyer & Simard, 2007)

The most rigorous modern test suite. Contains three batteries:

SmallCrush: Quick screen, 10 tests

Crush: 96 tests

BigCrush: 106 tests — the gold standard, takes hours to run

A well-implemented CSPRNG should pass all 106 BigCrush tests. Mersenne Twister, despite its statistical excellence, fails two BigCrush tests (LinearComp and MatrixRank) because its outputs have detectable linear structure.

DieHarder: An extended and revised version of Diehard, developed by Robert Brown at Duke University. Freely available and widely used.

6.3 Running NIST Tests in Practice

Here is how to generate a test file and run NIST SP 800-22 against it:

// Generate a 10MB binary file for NIST testing
public static void GenerateNistTestFile(string outputPath, int megabytes = 10)
{
    long bytesNeeded = megabytes * 1024L * 1024L;
    using var fileStream = new FileStream(outputPath, FileMode.Create);

    const int bufferSize = 65536;
    byte[] buffer = new byte[bufferSize];
    long written = 0;

    while (written < bytesNeeded)
    {
        int toWrite = (int)Math.Min(bufferSize, bytesNeeded - written);
        RandomNumberGenerator.Fill(buffer.AsSpan(0, toWrite));
        fileStream.Write(buffer, 0, toWrite);
        written += toWrite;
    }

    Console.WriteLine($"Generated {megabytes}MB test file: {outputPath}");
    Console.WriteLine($"Submit to NIST SP800-22 test suite for certification.");
}

The NIST test suite is open-source (written in C) and can be run locally before submitting to a lab. The practical workflow:

Generate 10M+ random numbers using your SlotRng implementation

Write them to a binary file

Run NIST SP 800-22 locally → fix any failures

Submit the same implementation to the certification lab

Lab runs their own tests independently and issues a certificate

6.4 The Chi-Square Test: A Quick Sanity Check

Before running a full battery, the chi-square test is a quick smoke test you can run in your own code:

/// <summary>
/// Chi-square goodness-of-fit test for uniform distribution.
/// Tests whether values in [0, bucketCount) are uniformly distributed.
/// Returns the p-value (should be > 0.01 for a good RNG).
/// </summary>
public static double ChiSquareTest(int bucketCount, int sampleCount)
{
    var observed = new long[bucketCount];

    for (int i = 0; i < sampleCount; i++)
    {
        int value = RandomNumberGenerator.GetInt32(bucketCount);
        observed[value]++;
    }

    double expected = (double)sampleCount / bucketCount;
    double chiSquare = 0.0;

    foreach (long count in observed)
    {
        double delta = count - expected;
        chiSquare += (delta * delta) / expected;
    }

    // Degrees of freedom = bucketCount - 1
    int df = bucketCount - 1;

    // p-value using incomplete gamma function (chi-square CDF)
    double pValue = 1.0 - ChiSquareCdf(chiSquare, df);

    Console.WriteLine($"Chi-square statistic: {chiSquare:F4}");
    Console.WriteLine($"Degrees of freedom:   {df}");
    Console.WriteLine($"p-value:              {pValue:F6}");
    Console.WriteLine($"Result: {(pValue > 0.01 ? "PASS ✓" : "FAIL ✗")}");

    return pValue;
}

// Usage:
// ChiSquareTest(bucketCount: 32, sampleCount: 3_200_000);
// Expected: chi-square ≈ 31 ± 15, p-value ≈ 0.3–0.7

Interpreting the p-value:

p < 0.01: Almost certainly not uniformly distributed — fail

p = 0.01–0.05: Suspicious — investigate further

p = 0.05–0.95: Normal range — pass

p > 0.99: Suspiciously uniform — also investigate (could indicate a fake random source that's "too perfect")

Note: the p-value itself is expected to be uniformly distributed across [0,1] for a good RNG. Running the test 100 times and checking that about 5 runs give p < 0.05 is a meta-test worth doing.


Part VII. Provably Fair Systems

7.1 What Is Provably Fair?

Provably Fair is a cryptographic protocol that allows players to independently verify that each game outcome was determined before the spin began and was not manipulated after the fact.

The core mechanism uses a commit-reveal scheme:

SERVER generates a secret seed (Server Seed)

SERVER hashes it: Commitment = SHA256(ServerSeed)

SERVER sends Commitment to the player BEFORE the spin

PLAYER provides their own Client Seed

SPIN outcome = f(ServerSeed, ClientSeed, SpinNonce)

After the session, SERVER reveals ServerSeed

PLAYER verifies: SHA256(ServerSeed) == Commitment (server didn't change the seed)

PLAYER recomputes outcome using revealed ServerSeed → result matches → fair

The player can prove retroactively that the server committed to a specific seed before the spin, that the seed was not changed after the player's bet was placed, and that the outcome calculation is transparent and correct. No trust is required.

7.2 Full Provably Fair Implementation in C#

public class ProvablyFairSession
{
    public string SessionId { get; }
    public string Commitment { get; }            // SHA256(ServerSeed) — shared with player upfront
    public bool   IsRevealed { get; private set; }

    private readonly byte[] _serverSeed;         // Kept secret until session ends
    private long _spinNonce = 0;                 // Increments with every spin

    private ProvablyFairSession(string sessionId, byte[] serverSeed, string commitment)
    {
        SessionId  = sessionId;
        _serverSeed = serverSeed;
        Commitment = commitment;
    }

    /// <summary>
    /// Creates a new Provably Fair session.
    /// The commitment (hash) must be sent to the player immediately.
    /// The server seed must NOT be revealed until the session ends.
    /// </summary>
    public static ProvablyFairSession Create()
    {
        var sessionId   = Guid.NewGuid().ToString("N");
        var serverSeed  = new byte[32];
        RandomNumberGenerator.Fill(serverSeed);
        var commitment  = Convert.ToHexString(SHA256.HashData(serverSeed)).ToLowerInvariant();

        return new ProvablyFairSession(sessionId, serverSeed, commitment);
    }

    /// <summary>
    /// Generates reel stops for a spin.
    /// Deterministic given the same server seed, client seed, and nonce.
    /// </summary>
    public int[] GenerateSpinStops(
        string clientSeed,
        IReadOnlyList<int> reelSizes)
    {
        var stops = new int[reelSizes.Count];

        for (int reelIndex = 0; reelIndex < reelSizes.Count; reelIndex++)
        {
            stops[reelIndex] = DeriveReelStop(
                _serverSeed,
                clientSeed,
                _spinNonce,
                reelIndex,
                reelSizes[reelIndex]
            );
        }

        _spinNonce++;
        return stops;
    }

    /// <summary>
    /// Reveals the server seed at the end of the session.
    /// After calling this method, no more spins can be generated.
    /// </summary>
    public string RevealServerSeed()
    {
        IsRevealed = true;
        return Convert.ToHexString(_serverSeed).ToLowerInvariant();
    }

    /// <summary>
    /// Verifies that a revealed server seed matches the original commitment.
    /// Can be called by the player's client or any third party.
    /// </summary>
    public static bool VerifyCommitment(string revealedSeed, string commitment)
    {
        byte[] seedBytes   = Convert.FromHexString(revealedSeed);
        byte[] actualHash  = SHA256.HashData(seedBytes);
        string actualHex   = Convert.ToHexString(actualHash).ToLowerInvariant();
        return actualHex == commitment.ToLowerInvariant();
    }

    /// <summary>
    /// Derives a single reel stop deterministically.
    /// Uses HMAC-SHA256 to combine server seed, client seed, nonce, and reel index.
    /// </summary>
    public static int DeriveReelStop(
        byte[]  serverSeed,
        string  clientSeed,
        long    nonce,
        int     reelIndex,
        int     reelSize)
    {
        // Build the HMAC message: clientSeed:nonce:reelIndex
        string  message      = $"{clientSeed}:{nonce}:{reelIndex}";
        byte[]  messageBytes = Encoding.UTF8.GetBytes(message);

        // HMAC-SHA256(key=serverSeed, message=clientSeed:nonce:reelIndex)
        byte[] hmacOutput = HMACSHA256.HashData(serverSeed, messageBytes);

        // Use the first 4 bytes to derive an integer
        uint rawValue = BitConverter.ToUInt32(hmacOutput, 0);

        // Unbiased reduction to [0, reelSize)
        return (int)(rawValue % (uint)reelSize);
        // Note: for production, use proper unbiased reduction (rejection sampling)
        // The modulo bias here is < 1/2^32 — negligible for reel sizes ≤ 64
    }
}

/// <summary>
/// Audit record stored per spin — sufficient to fully reconstruct any outcome.
/// </summary>
public record SpinAuditRecord(
    string SessionId,
    string Commitment,       // SHA256 of server seed — shared with player before session
    string ClientSeed,       // Provided by player
    long   Nonce,            // Spin number within session
    int[]  ReelStops,        // Actual stops generated
    int[]  ReelSizes,        // Reel sizes used
    decimal BetAmount,
    decimal WinAmount,
    DateTime Timestamp
);

7.3 Provably Fair Verification Flow

From the player's perspective, verification works like this:

After session ends, player receives:
  - ServerSeed (hex): "a3f9b2c1d4e5f6a7..."
  - Commitment (hex): "7d2a1b3c..."        ← was shared before any spin

Player verifies:
  SHA256("a3f9b2c1d4e5f6a7...") == "7d2a1b3c..."  → ✓ Server didn't change the seed

Player re-derives spin #42:
  HMAC-SHA256(
    key     = "a3f9b2c1d4e5f6a7...",
    message = "{clientSeed}:42:0"   ← reel 0
  ) → first 4 bytes → reel stop = 17

  Reel stop 17 on reel 0 → symbol: DIAMOND

  Player verifies this matches the actual spin result → ✓ Outcome was not manipulated

This can be implemented in JavaScript and run entirely client-side — no trust in any third party required.

7.4 Provably Fair in Regulated Markets

It's worth noting that Provably Fair, while technically elegant, has a complex relationship with traditional gambling regulation:

Crypto casinos (operating without a traditional gaming licence) widely adopt Provably Fair as a substitute for regulatory oversight

Licensed operators (MGA, UKGC, etc.) typically do not require Provably Fair — they rely on certified RNG and third-party audits instead

Some regulators view Provably Fair's use of a client seed as a potential fairness concern (theoretically, a player could select a client seed after seeing the commitment, which changes the outcome derivation)

The practical recommendation: implement Provably Fair in addition to, not instead of, a properly certified CSPRNG and standard audit logging.


Part VIII. Certification: What Labs Actually Test

8.1 The Major Certification Laboratories

Laboratory

Common Name

Headquarters

Key Markets

Gaming Laboratories International

GLI

New Jersey, USA

Global — the most widely accepted

BMM Testlabs

BMM

Las Vegas, USA

Americas, Europe, Australia

iTech Labs

iTech

Melbourne, Australia

Europe, Asia-Pacific

eCOGRA

eCOGRA

London, UK

Online gambling

NMi

NMi

Ede, Netherlands

Netherlands (KSA), Europe

Gaming Associates

GA

Gold Coast, Australia

Australia, Asia

For most B2B slot developers targeting European and international markets, GLI certification (specifically GLI-11 for online slots) is the baseline requirement.

8.2 GLI-11: The RNG Testing Standard

GLI-11 (Standards for Random Number Generators) is the authoritative standard for RNG testing in gaming. Key requirements:

Statistical Requirements (Section 3):

The RNG must pass all tests in the NIST SP 800-22 test suite

Tests must be run on a minimum of 1 gigabit (125 MB) of output

The significance level for each test must be α = 0.01

Tests must be run at least twice on independently generated samples

Seeding Requirements (Section 4):

The seed must be unpredictable — derived from a certified entropy source

The seeding mechanism must be documented and auditable

Re-seeding procedures must be defined and documented

It must be impossible to set the seed to a specific value externally

Implementation Requirements (Section 5):

The RNG must be isolated from all game logic (cannot be influenced by external inputs after seeding)

The RNG must produce outcomes independently of bet size, player identity, or prior outcomes

No mechanism may exist to predict, manipulate, or set the RNG output

The RNG algorithm must be documented precisely enough to allow independent reimplementation

Audit Requirements (Section 6):

Every spin must be fully logged with sufficient data to reconstruct any outcome

Logs must be tamper-evident and stored securely

The certification lab must be able to replay any historical spin from the audit log

Periodic Testing (Section 7):

After certification, the RNG is typically re-tested annually or after any code change

Some jurisdictions require continuous monitoring

8.3 The Certification Process: Step by Step

Phase 1: Pre-Submission (2–4 weeks)
  ├── Run NIST SP 800-22 locally — fix any failures
  ├── Run Diehard / DieHarder locally
  ├── Document the RNG algorithm precisely (algorithm, seeding, reseeding)
  ├── Document audit logging architecture
  └── Prepare Technical Submission Package (TSP)

Phase 2: Submission
  ├── Submit TSP to lab (GLI, BMM, etc.)
  ├── Lab assigns a project number and testing engineer
  └── Lab accesses the game via test environment or binary submission

Phase 3: Lab Testing (4–8 weeks)
  ├── Source code review (static analysis, algorithm verification)
  ├── Statistical tests on 125MB+ of generated output
  ├── Seeding security review
  ├── Audit log integrity testing
  ├── Replay verification (can spin #N be reproduced from logs?)
  └── Edge case testing (low/high bets, currency variants, etc.)

Phase 4: Issue Resolution
  ├── Lab issues test findings (each finding is a fail/pass/advisory)
  ├── Developer fixes all fails and resubmits affected components
  └── Lab retests — repeat until no fails

Phase 5: Certificate Issuance
  ├── Lab issues RNG Certificate (valid for 1–2 years typically)
  ├── Certificate references the specific software version tested
  └── Certificate is provided to operators and regulators on request

8.4 What Triggers a Certification Failure

The most common RNG-related certification failures:

Modular bias. Using value % range without rejection sampling. Even a bias of 0.0001% is a finding.

Predictable seeding. Any seed that incorporates time, process ID, or other observable system state without CSPRNG entropy will fail the seeding review.

State persistence across sessions. If the RNG state from one player session can influence another session's outcomes, this is a critical failure.

Missing audit data. If any field needed to reconstruct a spin is not logged, the audit review fails — even if the RNG itself is perfect.

Non-atomic transactions. If the RNG output is generated before the bet is accepted (and could potentially be reused if the bet fails), this creates a potential exploit.

Undocumented parameters. If the algorithm description in the submission doesn't match the actual implementation, the code review will catch it.


Part IX. Integrating RNG into the Game Server

9.1 The Principle of RNG Isolation

Certification standards require that the RNG be isolated — its output cannot be influenced by game logic, player identity, or any external input after the initial seeding. In code, this translates to a clean dependency boundary:

//CORRECT — RNG is a pure dependency, injected and isolated
public class SpinProcessor
{
    private readonly ISlotRng       _rng;
    private readonly IWinCalculator _calculator;
    private readonly IWalletService _wallet;

    public SpinProcessor(ISlotRng rng, IWinCalculator calculator, IWalletService wallet)
    {
        _rng        = rng;
        _calculator = calculator;
        _wallet     = wallet;
    }

    public async Task<SpinResult> ProcessSpinAsync(SpinRequest request)
    {
        // Step 1: Generate reel stops — pure RNG operation, no external input
        int[] stops = _rng.NextReelStops(request.Config.ReelSizes);

        // Step 2: Calculate wins from stops — deterministic, no RNG
        var wins = _calculator.CalculateWins(stops, request.Config);

        // Step 3: Wallet operations — separate concern
        await _wallet.DebitAsync(request.SessionToken, request.BetAmount);
        if (wins.TotalWin > 0)
            await _wallet.CreditAsync(request.SessionToken, wins.TotalWin);

        return new SpinResult(stops, wins);
    }
}

Notice that NextReelStops takes only the reel sizes as input — no player ID, no bet amount, no previous outcome. The RNG is blind to everything except its own internal state and the structural parameters of the reels.

9.2 Audit Logging for Every Spin

public class AuditingSpinProcessor : ISpinProcessor
{
    private readonly ISpinProcessor    _inner;
    private readonly ISpinAuditLogger  _auditLogger;

    public async Task<SpinResult> ProcessSpinAsync(SpinRequest request)
    {
        var spinId    = Guid.NewGuid();
        var timestamp = DateTime.UtcNow;

        SpinResult result;
        try
        {
            result = await _inner.ProcessSpinAsync(request);
        }
        catch (Exception ex)
        {
            await _auditLogger.LogFailedSpinAsync(new FailedSpinRecord(
                SpinId:    spinId,
                SessionId: request.SessionToken,
                Reason:    ex.Message,
                Timestamp: timestamp
            ));
            throw;
        }

        // Every successful spin gets a complete audit record
        await _auditLogger.LogSpinAsync(new SpinAuditRecord(
            SpinId:     spinId,
            SessionId:  request.SessionToken,
            Timestamp:  timestamp,
            BetAmount:  request.BetAmount,
            ReelStops:  result.Stops,           // ← sufficient to replay any outcome
            ReelSizes:  request.Config.ReelSizes,
            WinAmount:  result.TotalWin,
            RngVersion: SlotRng.AlgorithmVersion // ← version tag for future audits
        ));

        return result;
    }
}

Part X. Common Mistakes and How to Avoid Them

Mistake 1: Using System.Random for Any Part of the Spin

csharp

// ❌ NEVER — even for "non-critical" parts like visual effects
var rng = new Random();
int particleCount = rng.Next(5, 15);  // "just for animation"

The moment any component uses System.Random, a certification lab will flag it. Even if it doesn't affect outcomes, the presence of a weak RNG in the codebase is a finding. Use RandomNumberGenerator everywhere.

Mistake 2: Caching RNG Output

csharp

// ❌ DANGEROUS — pre-generating and caching RNG output
private readonly Queue<int> _cachedStops = new();

// Pre-fill on server start
for (int i = 0; i < 10000; i++)
    _cachedStops.Enqueue(RandomNumberGenerator.GetInt32(32));

// Use from cache on each spin
int stop = _cachedStops.Dequeue();

This pattern creates a window where the cached outputs could be observed, logged, or accessed — compromising future spin security. Generate numbers at the moment they are needed, not before.

Mistake 3: Sharing RNG State Between Sessions

csharp

// ❌ WRONG — one RNG instance shared across all player sessions
public class GameServer
{
    private static readonly SlotRng _globalRng = new();  // shared state

    public SpinResult Spin(SpinRequest request)
    {
        return _globalRng.GenerateStops(request.ReelSizes);
    }
}

While RandomNumberGenerator.GetInt32() is thread-safe, the issue here is architectural: if session A's spins influence the sequence seen by session B (because they share state), the certification requirement of independent per-session outcomes is violated. Design sessions to be independent.

Mistake 4: Generating the RNG Output Before the Bet Is Accepted

csharp

// ❌ WRONG — race condition / potential manipulation
int[] stops = _rng.NextReelStops(config.ReelSizes);  // generated first

bool betAccepted = await _wallet.DebitAsync(token, bet);
if (!betAccepted)
{
    // Discard the stops? Or use them for the next spin?
    // Either way — the outcome was generated before the bet was confirmed
}

The correct order: debit → generate → evaluate → credit. The RNG output must be generated after the bet is irrevocably committed.

Mistake 5: No Version Tag on the RNG Algorithm

csharp

// ✅ Always version-tag your RNG implementation
public sealed class SlotRng : ISlotRng
{
    public const string AlgorithmVersion = "CSPRNG-OS-v1.2";
    // "OS" = backed by OS entropy pool (BCryptGenRandom / /dev/urandom)
    // "v1.2" = specific implementation version

    // Stored in every audit record so future re-certification
    // knows exactly which algorithm produced which outputs
}

When a bug is found two years from now, you need to be able to tell a regulator: "spin #4,281,042 was generated by CSPRNG-OS-v1.1, which had the following specification." Without version tags in the audit log, this is impossible.


Summary

The RNG is not just another module in your game server. It is the cryptographic foundation of player trust and regulatory compliance. Every design decision — from algorithm choice to seeding strategy to audit logging — has consequences that extend from user experience to legal liability.

The key principles to carry forward:

Never use System.Random or any non-cryptographic PRNG in a real-money gaming context. The performance advantage is irrelevant; the risk is not.

Use the OS CSPRNG via RandomNumberGenerator in .NET. It already implements a hybrid model combining hardware entropy (TRNG) with a cryptographically secure algorithm. It is thread-safe, auditable, and certifiable.

Reject modular bias in all range reduction operations. Use RandomNumberGenerator.GetInt32(n) which handles this correctly, or implement rejection sampling manually.

Seed from entropy, never from time. A seed derived from a timestamp, process ID, or any other observable system state is exploitable.

Log every spin completely. Enough data to fully reproduce any outcome from the audit log — reel stops, reel sizes, RNG version — stored tamper-evidently.

Test before you certify. Run NIST SP 800-22 and a chi-square test locally before submission. Certification labs expect a clean first submission; repeated failures are expensive and damage your relationship with the lab.

Implement Provably Fair as an additional layer, not as a substitute for proper certification. In licensed jurisdictions, certification is non-negotiable.

Neon Royale use cookies for technical purposes, functionality and measurement.

Policy