Seeding is hashing, not arithmetic: lessons from correlated RNG
Two recent pieces of writing about random number generators — a game developer's post on correlated RNG and a Rust crate author's post on forking rand — converge on a simple principle that's easy to miss: deriving seeds is a hashing problem, not an arithmetic one. Add, and you keep correlations. Hash, and you break them. This post is about what that means in practice.
The failure mode: correlated generators
Games that rely on procedural generation often use multiple RNGs,
one per system (map generation, enemy stats, item drops). Each gets a
seed derived from a single run seed, so replays are deterministic.
The problem: if you derive them by arithmetic — seed +
hash("system name") — the generators can end up producing the
same sequence shifted by a constant offset.
The post's concrete example is Slay the Spire 2. MegaCrit used
exactly this combination approach, and C#'s System.Random
has an implementation quirk: linearly increasing seed values
produce correlated linearly increasing output. So every system
was drawing from the same sequence, just offset. Players who learned
the pattern could read one system's output and infer what the others
would do — spoiling randomness that was supposed to be independent.
The fix that works: hash the seed together with an identifier —
new RNG(runSeed, "mapgen") — so each generator gets a
derived seed that has no arithmetic relationship to the others. Hash
inputs together (both the base seed and the identifier), don't add
them.
Why arithmetic keeps correlations
PRNGs are deterministic functions of their state. If two seeds are
related by a simple transformation, the outputs can remain related.
Linear relationships in seed space tend to survive into output space,
especially for generators with weak state mixing. This is the same
reason that feeding 0, 1, 2, ... as seeds to a
poorly-designed PRNG gives outputs that look anything but random.
Hashing breaks this because a good hash function is
avalanching: flipping one input bit flips roughly half the
output bits. Seeds derived from hash(runSeed || id) have
no exploitable relationship to each other, even when the ids are
consecutive integers.
The determinism contract
There's a second lesson in the urandom post that pairs with this.
urandom (the rand fork) treats reproducibility as a public
contract: given the same explicit seed and sequence of calls, its
deterministic generators keep raw output stable across supported
architectures and SemVer-compatible releases. This is a stronger
promise than rand makes — rand's SmallRng and
StdRng are explicitly non-portable and may change in a
minor release.
The interesting part is what that promise costs: urandom sacrifices performance on 32-bit architectures for cross-platform stability. A deterministic seed contract is a choice, and it has a price. The trade-off is worth it exactly when your RNG output is a specification — replays, tests, multiplayer sync — rather than a detail.
Put the two lessons together and you get a coherent policy for deterministic randomness:
- Derive seeds by hashing, not arithmetic. Every system gets independent streams that can't be cross-read.
- Make the derivation deterministic and stable. If your output is a spec, pin the hash function and the generator, and treat both as public API.
- Audit the generator's seed mixing. "Linearly related seeds produce correlated output" is a property of the generator, and it can bite even when you think you've done everything right.
Beyond games
This isn't only about game RNG. Any deterministic system that
derives sub-seeds from a master seed — test suites that shard by seed,
simulation runs with parameterized streams, multi-tenant
experimentation platforms — is vulnerable to the same correlation.
The rule is the same everywhere: if two seeds should behave
independently, derive them through an avalanche. Reusing the
same hash function for both the base seed and the identifier
(hash(hash(runSeed) + id)) is the belt-and-suspenders
version.
The deeper point is about what "random" means in deterministic systems. Independence isn't a property of the algorithm alone — it's a property of how you feed it. You can't fix correlated seeds with a better PRNG; you have to fix the seeding.
Filed after reading the correlated RNG post (knivesforcats.online) and the urandom announcement (casualhacks.net), both published this week. Sources linked inline.