Understanding how to use Mulberry32 to achieve deterministic randomness in JavaScript

Talking about Javascript.

Randomness is often treated as a black box. In JavaScript, Math.random() is usually taken at face value: it produces numbers that “look random”, so it must be good enough. In practice, however, this approach quickly breaks down as soon as reproducibility becomes important. Debugging procedural systems, replaying simulations, generating the same level twice, or restoring a game state after loading all require one essential property: determinism.

This is where seeded pseudo-random number generators come in. Mulberry32 is one of the simplest and most pragmatic examples of such a generator. Despite its small size, it demonstrates clearly how deterministic randomness works and why certain design choices exist.

Mulberry32 is a 32-bit deterministic pseudo-random number generator. Deterministic means that the entire sequence of numbers it produces is fully determined by its initial state, commonly called the seed. Internally, the generator stores a single unsigned 32-bit integer and updates it every time a new random value is requested. The updated state is then mixed through a sequence of bitwise operations and integer multiplications before being converted into a floating-point number.

In JavaScript, this works reliably because bitwise operators implicitly operate on 32-bit integers, even though all numbers are technically IEEE 754 floating-point values. Mulberry32 takes advantage of this behavior to emulate true 32-bit arithmetic.

A clear and explicit JavaScript implementation using a class looks like this:

JavaScript
class Mulberry32 {
    constructor(seed) {
        this.seed = seed >>> 0;
    }

    next() {
        let t = this.seed += 0x6D2B79F5;
        t = Math.imul(t ^ (t >>> 15), t | 1);
        t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
        return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
    }
}

This class represents exactly what Mulberry32 is: a small state machine. The constructor initializes the internal state, forcing it into an unsigned 32-bit range using the unsigned right shift operator. The next() method advances the state and produces the next pseudo-random value.

The constant 4294967296 used in the final division is equal to 2³². After the mixing phase, the internal value is an unsigned 32-bit integer between 0 and 2³²?1. Dividing by 2³² maps this integer into a floating-point value in the half-open interval [0, 1). This ensures that the result behaves exactly like Math.random(), including the important property that it will never return 1.

The increment constant 0x6D2B79F5 is equally important. It advances the internal state by a fixed amount on every call. Because this value is odd and well distributed at the bit level, it guarantees full traversal of the 32-bit state space modulo 2³² and avoids short cycles or obvious patterns. Mulberry32 is effectively walking through the integer space in large, decorrelated steps and hashing each position it visits.

Using the generator correctly is straightforward, but it requires a clear mental model. A seed does not generate a single random number. A seed defines an entire sequence. To generate five reproducible random numbers, the generator must be initialized once and then advanced five times:

JavaScript
const rng = new Mulberry32(12345);

const values = [];
for (let i = 0; i < 5; i++) {
    values.push(rng.next());
}

console.log(values);

Running this code will always produce the same five values, regardless of browser, operating system, or execution time. This is the fundamental advantage of a deterministic generator. Re-initializing the generator inside the loop would reset its state on every iteration and would therefore produce the same value repeatedly, which is a common mistake when first working with seeded randomness.

At this point, the relevance of Mulberry32 for videogame development becomes evident. Games rely heavily on randomness, but they also rely on consistency. Enemy behavior, loot drops, procedural levels, particle effects, and AI decisions often appear random to the player, yet must be reproducible for debugging, replays, multiplayer synchronization, or save/load systems.

Using Mulberry32 inside a game loop allows randomness to become a controlled subsystem rather than an external source of entropy. If the same seed is used at the start of a level, the same procedural layout can be regenerated every time. If the seed is stored in a save file, the exact same random events can be replayed after loading. If the generator is stepped deterministically each frame, even complex emergent behavior can be reproduced precisely.

For example, a procedural level generator might consume random values in a fixed order to decide room placement, enemy spawns, and item drops. As long as the sequence of calls remains the same, the resulting level will be identical. This makes balancing, testing, and iteration dramatically easier. A bug reported by a tester can often be reproduced simply by reusing the same seed.

Mulberry32 is also particularly well suited for games because of its simplicity and performance. It is fast enough to be called every frame without concern, and its state is small enough to be copied, reset, or branched. Multiple independent generators can be created for different subsystems, such as one for world generation and another for combat randomness, without introducing unintended correlations.

Once this model is understood, many practical use cases fall naturally into place. Because the entire state of the generator is contained in a single integer, saving and restoring it is trivial:

JavaScript
const savedSeed = rng.seed;

Later, restoring the sequence is as simple as assigning the seed back:

JavaScript
rng.seed = savedSeed;

From that moment on, every call to next() will produce exactly the same values it would have produced originally. This property is invaluable for procedural generation, replay systems, deterministic physics simulations, and debugging scenarios where a rare bug must be reproduced reliably.

Mapping the generated values to useful ranges follows the same rules as with Math.random(). If an integer in a given range is required, the floating-point output can be scaled and truncated:

JavaScript
function randInt(rng, min, max) {
    return Math.floor(rng.next() * (max - min + 1)) + min;
}

As long as the generator is advanced consistently, the resulting integers will be just as reproducible as the underlying floating-point values.

Mulberry32 is not cryptographically secure, and it is not intended to be. Its purpose is not to resist attacks, but to provide fast, predictable, and repeatable pseudo-randomness. Its simplicity is a strength rather than a weakness. Every line can be understood, every constant has a reason to exist, and the behavior of the generator can be reasoned about precisely.

Once randomness stops being treated as magic and starts being treated as state plus transformation, generators like Mulberry32 become reliable tools instead of mysterious black boxes. In JavaScript, where determinism is often overlooked, this small class can make a disproportionate difference in the quality and debuggability of complex systems.

Look at this working example:

Enter a seed between 0 and 4,294,967,295 and you will see the same sequence of 10 pseudo-random numbers every time.

Changing the seed generates a completely different sequence, while using the same seed always produces identical results. This makes the example useful for understanding how deterministic randomness works and how a single number can fully define an entire random sequence.

And this is the source code:

HTML
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <style>
            body {
                font-family: monospace;
                background: #111;
                color: #eee;
                padding: 20px;
            }

            input, button {
                background: #222;
                color: #eee;
                border: 1px solid #444;
                padding: 6px 10px;
                font-family: inherit;
            }

            button {
                cursor: pointer;
            }

            pre {
                margin-top: 20px;
                padding: 10px;
                background: #000;
                border: 1px solid #333;
            }
        </style>
    </head>
    <body>
        Seed: <input id="seed" type="text" value="12345">
        <button id="generate">Generate</button>
        <pre id="output"></pre>
        <script>
            class Mulberry32 {
                constructor(seed) {
                    this.seed = seed >>> 0;
                }
                next() {
                    let t = this.seed += 0x6D2B79F5;
                    t = Math.imul(t ^ (t >>> 15), t | 1);
                    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
                    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
                }
            }
            document.getElementById('generate').onclick = () => {
                const seedValue = parseInt(document.getElementById('seed').value, 10) || 0;
                const randomNumber = new Mulberry32(seedValue);
                let output = '';
                for (let i = 0; i < 10; i ++) {
                    const value = Math.floor(randomNumber.next() * 100);
                    output += value + '\n';
                }
                document.getElementById('output').textContent = output;
            };
        </script>
    </body>
</html>

Will you use Mulberry32 in your games?