You Don't Want Randomness, You Want Reproducibility
Writing a random number generator is one of the standard warnings, alongside crypto and date libraries. Don't reinvent any of the three.
I have shipped my own three times, across twenty years, in three languages. Two of them were right and one was worse than writing nothing at all, and sorting out which is which turns on a distinction the word random actively obscures.
The Engine Copied the Standard, Exactly
My third engine's maths library carries this:
unsigned int RNG::Get()
{
m_nSeed = m_nSeed * 1103515245 + 12345;
return (uint32_t)(m_nSeed / 65536) % s_nMax;
}With s_nMax declared as 32768 just above it. Anyone who has read the C standard will recognise it immediately, because that is the example implementation of rand() printed in the standard itself, constant for constant: the same multiplier, the same increment, the same division by 65536, the same modulus.
I did not invent a generator. I copied the most famous mediocre one in existence, and its mediocrity is well documented. The low bits are weak, the period is short by modern standards, and the values fail statistical tests that any modern generator passes without effort.
Copying it anyway was correct, and the reason is one line elsewhere in the codebase:
m_RNG.Seed(m_unId);That is a galaxy chunk seeding a generator from its own id, so that a chunk's contents are a pure function of where it is. Fly away and come back and the same stars are there, because they were never stored, only recomputed.
That trick has a requirement, and the requirement is not quality. It is that Get() returns the same sequence for the same seed everywhere. The same on Windows and iOS. The same in the editor and the packaged build. The same next year, after an SDK upgrade, on a compiler that did not exist when the code was written.
The platform's rand() promises none of that. It promises numbers without an obvious pattern, and the language specifications are explicit that the sequence is implementation-defined. Two C libraries can both be perfectly conformant and disagree completely. Two runs of the same binary on two platforms can produce two different galaxies.
So the engine isn't carrying its own generator because the standard one is bad. It's carrying it because the standard one is unspecified, and an unspecified generator cannot be the basis of anything reproducible. Copying the C standard's example is not an attempt at a better generator. It is pinning one.
The Site Does the Same Thing for a Different Reason
Twenty years later, the generator that draws the placeholder cards on this site does it again:
// mulberry32. Small, fast, and good enough that a one-character change to a slug
// gives an unrelated card rather than a nudged version of the same one.
function rng(seed) {
let a = seed >>> 0;
return () => {
a = (a + 0x6d2b79f5) >>> 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
Seeded from an FNV-1a hash of the post's slug. Same structural decision as the engine, and the same reason underneath it: Math.random() cannot be seeded at all, so a card would be different on every build, and a card that changes when nothing changed is worse than no card.
But the comment is doing something the engine's version doesn't, and it names the property that mattered here. Mulberry32 avalanches, so slugs that differ by one character produce unrelated outputs rather than neighbouring ones. A hash plus a weak generator would have given a-particle-is-nine-numbers and a-particle-is-eleven-numbers two nearly identical cards, and the whole point is that a card identifies a post at a glance.
That's the second reason to ship a hand-written one. Not just "the platform's is unspecified", but "the platform's doesn't expose the property I actually depend on". Math.random() has no seed. There is nothing to configure. Writing eight lines is the only route to the requirement.
And Once, It Was Strictly Worse Than Nothing
The card server from 2017 opens two of its request handlers like this:
mt_srand(time());
Then, further down the same file, it rolls the rarity of every card in a pack:
$roll = mt_rand(0, RULE_RARITY_WEIGHT_POOL);
This is the same instinct pointed at nothing, and it is actively harmful.
PHP seeds its generator automatically, per request, from a source with far more entropy than the clock. Writing mt_srand(time()) replaces that with a seed that has one second of resolution. Every request arriving in the same second gets the identical sequence.
For a pack opener, that is not a subtle statistical weakness. It is an exploit. Open two packs in the same second and get the same cards. Two players opening packs simultaneously get the same pull. The line that looks like it is adding randomness has removed almost all of it, and it has done so in the one place where a user has both the motive and the ability to notice.
The tell is that the code takes no benefit from being seeded. Nothing recomputes a pack from its id. Nothing needs the same pull twice. There is no reproducibility requirement anywhere in the file, so the seed serves no purpose, and a seed that serves no purpose can only make things worse. Deleting the line is strictly better than keeping it.
I wrote all three of these. The difference between the good ones and this one isn't skill or care. It's that in the first two I could say what the seed was for, and here I had taken on "always seed the random number generator" as a rule rather than as an answer to a question.
The Distinction
Almost nobody needs their own randomness. What they need is their own reproducibility, and the word random is what hides that, because it makes the quality of the numbers sound like the point.
Once the question is reproducibility, the decision gets easy, and it's three questions in order:
Does anything need the same sequence twice? Same seed, two platforms. Same seed, two years apart. Same seed, editor and shipped build. If nothing does, use the platform generator, don't seed it, and stop.
Does the platform promise that? Almost always no. C's rand() is implementation-defined, Math.random() cannot be seeded at all, and language runtimes reserve the right to change generators between versions. A promise nobody made is not a promise anything can be built on.
Then pin one. Copy a small, published, named algorithm into the codebase where nothing can change it underneath. Mediocre and pinned beats excellent and unspecified, every time, for this purpose. The engine's copy of rand() is a bad generator and a perfectly good decision.
The only case that needs a genuinely good generator is when the numbers themselves must withstand scrutiny, which means anything security relevant, and there the answer is not to write one either. It's RandomNumberGenerator, crypto.getRandomValues, random_bytes. Two categories, and neither of them is "write a better rand()".
What Transfers
A seed is a claim that something must be recomputable. If the code cannot say what would go wrong when the sequence changes, the seed is decoration. Every seeded generator should have a one-sentence answer to "recomputable from what, and why", and if it doesn't, the fix is to remove the seed rather than improve the generator.
Unspecified is a worse problem than mediocre. Quality is measurable and its consequences are gradual. An unspecified sequence fails all at once, on the platform nobody tested, months later, and it looks like corruption rather than like randomness.
Copying a published algorithm is not inventing one. The warning against writing a random number generator exists for people inventing one. Pasting mulberry32 or the C standard's example into a codebase is the opposite of invention: it is choosing a known, named, unchanging thing over an unknown, unnamed, changing one.
Ask what a rule was protecting before applying it. "Always seed the RNG" is good advice for procedural generation and harmful advice for a web request handler. The rule I had taken on was a fragment of an argument, and I applied it in the one place where the argument ran the other way.