Random Seeds and Reproducibility
True randomness is great for security tokens but terrible for reproducibility. When you write a unit test that depends on random values, run a Monte Carlo simulation that colleagues need to verify, or generate procedural content in a game that players share via seed codes, you need deterministic pseudorandomness. A seeded PRNG produces the same sequence of values every time you initialize it with the same seed, making random behavior reproducible.
Why Reproducibility Matters
Three scenarios demand seeded randomness:
Testing: A randomized test that fails intermittently is a debugging nightmare. Fixed seeds let you reproduce the exact failure every time.
// Test with fixed seed — always reproducible
test('shuffle preserves all elements', () => {
const rng = new SeededPRNG(42)
const deck = [1, 2, 3, 4, 5]
const shuffled = fisherYatesShuffle(deck, rng)
expect(shuffled).toEqual([3, 1, 5, 2, 4]) // Deterministic
})
// Test with random seed logged on failure — reproducible after the fact
test('randomized partition invariant', () => {
const seed =.Date.now()
const rng = new SeededPRNG(seed)
try {
// ... test logic using rng ...
} catch (e) {
e.message += ` (seed: ${seed})`
throw e
}
})
Simulations: Scientific and financial simulations must be reproducible for peer review and regulatory compliance. If your Monte Carlo simulation produces different results every run, no one can verify your conclusions.
Game development: Games like Minecraft use world seeds so players can share identical procedurally generated worlds. The same seed always produces the same terrain, structures, and biome distribution.
Choosing a PRNG Algorithm
Not all PRNGs are equal. Here is how the common options compare:
| Algorithm | Period | Speed | Statistical Quality | Use Case |
|---|---|---|---|---|
| LCG (linear congruential) | 2³² | Very fast | Poor | Avoid |
| xorshift32 | 2³²−1 | Very fast | Moderate | Simple games |
| xorshift128+ | 2¹²⁸−1 | Very fast | Good | General purpose |
| PCG32 | 2⁶⁴ | Fast | Excellent | Recommended |
| Mersenne Twister (MT19937) | 2¹⁹⁹³⁷−1 | Moderate | Excellent | Scientific computing |
| xoshiro256** | 2²⁵⁶−1 | Very fast | Excellent | Modern standard |
For JavaScript applications, xoshiro256** provides the best balance of speed, quality, and reasonable state size. Here is a compact implementation:
class Xoshiro256SS {
constructor(seed) {
// SplitMix32 to expand seed into 4 state values
this.state = [0, 0, 0, 0]
for (let i = 0; i < 4; i++) {
seed += 0x9e3779b9
let t = seed ^ (seed >>> 16)
t = Math.imul(t, 0x21f0aaad)
t ^= t >>> 15
t = Math.imul(t, 0x735a2d97)
t ^= t >>> 15
this.state[i] = t >>> 0
}
}
next() {
const [s0, s1, s2, s3] = this.state
const result = Math.imul(s1, 5) >>> 0
const rotated = ((result << 7) | (result >>> 25)) >>> 0
const t = (s1 << 17) >>> 0
this.state[2] = (this.state[2] ^ t) >>> 0
this.state[0] = (s0 ^ s2) >>> 0
this.state[1] = (s1 ^ s3) >>> 0
this.state[2] = (s2 ^ s0) >>> 0
this.state[3] = (s3 ^ s1) >>> 0
this.state[0] = (this.state[0] ^ (this.state[0] >>> 16)) >>> 0
return (rotated >>> 0) / 4294967296
}
}
const rng = new Xoshiro256SS(12345)
rng.next() // 0.4690172740712762 — same every time with seed 12345
Seed Generation and Management
The seed itself should be random in production but fixed in tests. A common pattern:
const DEFAULT_SEED = 42
function createRNG(overrides = {}) {
const seed = overrides.seed ?? DEFAULT_SEED
return new Xoshiro256SS(seed)
}
// Production: random seed
const productionRNG = createRNG({ seed: crypto.getRandomValues(new Uint32Array(1))[0] })
// Testing: fixed seed
const testRNG = createRNG({ seed: 42 })
For simulations that run across multiple machines, store the seed alongside results:
function runSimulation(config) {
const seed = crypto.getRandomValues(new Uint32Array(1))[0]
const rng = new Xoshiro256SS(seed)
const results = computeModel(rng, config)
return {
seed, // Required for reproduction
config, // Full configuration
results, // Output data
timestamp: new Date().toISOString(),
}
}
Common Pitfalls
- Seeding with timestamps: Using
Date.now()as a seed produces different values per millisecond, but two simulations started in the same millisecond share a seed. Usecrypto.getRandomValues()for production seeds instead. - State mutation across tests: If you share a single PRNG instance across tests, the second test depends on how many values the first test consumed. Create a fresh PRNG per test.
- Floating-point precision: Different languages handle float conversion differently.
rng.next() / 2³²may produce slightly different values in JavaScript vs. Python due to double precision. For cross-language reproducibility, work with integer outputs only. - State serialization: Long-running simulations may need to checkpoint. Serialize the full PRNG state array so you can resume from the exact point of interruption.
// Checkpoint and restore
const checkpoint = [...rng.state]
// ... later ...
const restored = new Xoshiro256SS(0)
restored.state = checkpoint
Practice seeded random number generation with the tool at /tools/random-number, which supports both standard and seeded modes with selectable algorithms and exportable seed values.