One Seed, One World
Seeded randomness
The hand-drawn pond from last chapter cannot scale past the author's patience. The rule that replaces it is the world’s first randomness rule: every random draw in this world comes from a generator built from one explicit seed, so the seed names the world completely.
The map needs variation, some ingredient that decides where the water goes without you deciding for it. The obvious source is the operating system or the clock: something unpredictable.
The simulation has a stronger demand. When a creature drowns at tick 40,000 of a three-day run, you need to run those three days again, watch the same creature approach the same pond, and catch the bug in the act.
A world that comes out different every time cannot be debugged, cannot be tested, and cannot keep this volume's promise: a run of the server can be reproduced exactly.
The terrain must surprise you on the first run, then repeat itself, cell for cell, on every run after. That works because random does not have to mean unrepeatable. It can mean patternless to any consumer while completely determined by a starting number.
That starting number is called a seed. Tell someone the seed and you have told them where every rock, every pond, and every later seeded event came from. The proof is a tiny generator you can run in your head, the real one wired into the terrain grid, and the same world generated twice.
The toy generator
A random number generator is smaller than the name suggests. It holds one number, the state, and it has one rule for turning the current state into the next one.
Ask it for a number and it applies the rule, remembers the result, and hands the result to you. That is the whole machine.
Here is one small enough to run by hand: the state is a number from 0 to 15, and the rule is multiply by 5, add 3, keep the remainder after dividing by 16.
Start the state at 7 and turn the crank. Five sevens are 35, plus 3 is 38, and 38 divided by 16 leaves remainder 6: the first output is 6. From 6, 30 plus 3 is 33, remainder 1. From 1, the result is 8. From 8, 43 leaves remainder 11.
The machine seeded with 7 begins 6, 1, 8, 11, and there is no arithmetic in it you could not do in a checkout line. A program can crank it farther.
// cmd/worldd/main.go — a throwaway main; the real one returns in stage 2
package main
import "fmt"
// next is a toy random number generator: sixteen possible states,
// one rule for stepping between them.
func next(state int) int {
return (5*state + 3) % 16
}
func run(seed, n int) {
fmt.Printf("seed %2d:", seed)
state := seed
for i := 0; i < n; i++ {
state = next(state)
fmt.Printf(" %d", state)
}
fmt.Println()
}
func main() {
run(7, 18)
run(7, 18)
run(8, 18)
}
$ go run ./cmd/worldd
seed 7: 6 1 8 11 10 5 12 15 14 9 0 3 2 13 4 7 6 1
seed 7: 6 1 8 11 10 5 12 15 14 9 0 3 2 13 4 7 6 1
seed 8: 11 10 5 12 15 14 9 0 3 2 13 4 7 6 1 8 11 10
The two seed-7 lines are identical, and not by luck. The machine's next output depends only on its state, so the same starting state must produce the same sequence forever.
That is the property this whole book stands on, already present in a four-line function.
The sequence looks respectably scrambled: 6, 1, 8, 11, 10 hops around with no stride a glance can catch. Before any value repeats, all sixteen possible values have appeared exactly once.
Read seed 8's line against seed 7's. It is the same sequence, entered at a different point. With only sixteen states there is only one loop to walk, and every seed picks a doorway into it.
The toy is a linear congruential generator, and its rule in symbols is s′ = (a·s + c) mod m, with a=5, c=3, m=16 here. Check it against the hand crank: s=7 gives (5·7 + 3) mod 16 = 38 mod 16 = 6, the first output above.
The state can never leave 0..m−1, because a remainder after dividing by m cannot reach m. The sequence must eventually revisit a state, and the moment it does, it repeats forever.
The count of outputs before that happens is the generator's period. The run shows this one achieving the best possible: all 16, a full lap.
Everything wrong with the toy is quantity, not kind. Sixteen states means the terrain of a 96-cell map would visibly repeat before one pond finished.
The generator in Go's standard library, math/rand/v2, keeps 128 bits of
state instead of 4, so the loop it walks has about 3.4×1038 states. It
scrambles each state before showing it to you, so consecutive outputs share no visible
family resemblance.
The algorithm is called PCG, a permuted congruential generator: the same multiply-and-add heart you hand-cranked, with better output. What it does not change, because no PRNG changes it, is the contract. Seed it, and the entire sequence is fixed before the first draw.
Terrain from a seed
Now wire that contract into the ground. The generation strategy is deliberately humble: soil the interior as before, then drop a spring at a random interior cell and let the water wander.
Thirty times, the walker floods the cell under it and stumbles one cell in a random direction, refusing steps onto the rim. A path that doubles back floods cells it already flooded, so the pond comes out dense where the walk lingered: crude hydrology, but recognizably a pond, and grown instead of placed.
// internal/sim/gen.go
package sim
import "math/rand/v2"
// Generate builds a w-by-h world from one seed: a rock rim, a soil
// interior, and a pond carved by a random walk. The same seed always
// builds the same world.
func Generate(w, h int, seed uint64) *Grid {
rng := rand.New(rand.NewPCG(seed, 0))
g := NewGrid(w, h)
for y := 1; y < h-1; y++ {
for x := 1; x < w-1; x++ {
g.Set(Coord{X: x, Y: y}, Soil)
}
}
// Drop a spring somewhere in the interior, then let the water
// wander: at each step it floods the cell it stands on and
// stumbles one cell in a random direction, staying off the rim.
c := Coord{X: 1 + rng.IntN(w-2), Y: 1 + rng.IntN(h-2)}
steps := []Coord{{X: 1, Y: 0}, {X: -1, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: -1}}
for i := 0; i < 30; i++ {
g.Set(c, Water)
d := steps[rng.IntN(4)]
n := c.Offset(d.X, d.Y)
if n.X >= 1 && n.X <= w-2 && n.Y >= 1 && n.Y <= h-2 {
c = n
}
}
return g
}
// cmd/worldd/main.go — the real main again
package main
import (
"fmt"
"theworld/internal/sim"
)
const version = "0.0.1"
func main() {
const seed = 5
fmt.Println("worldd", version, "seed", seed)
g := sim.Generate(12, 8, seed)
fmt.Print(g.Render())
}
$ go run ./cmd/worldd
worldd 0.0.1 seed 5
############
#..........#
#..........#
#.~~~~~....#
#.~~~~~~...#
#...~~~~...#
#...~......#
############
rand.NewPCG(seed, 0) constructs the generator's 128-bit starting state
from two 64-bit words, and the two words do different jobs. The first is the seed,
the number that names the world.
The second picks a stream. Build two generators from one seed and different second words and you get two unrelated sequences, so a system can have draws of its own without sharing a position with somebody else's.
This book pins the convention here: terrain generation draws from stream 0, and the world's laws draw from stream 1. The stream numbers are constants in the code, never choices anyone makes at run time, so one number you choose still names everything.
rand.New wraps that raw engine in the type with the convenient methods.
rng.IntN(n) is the one this chapter leans on: a draw scaled into the
range 0 to n−1, fair across it.
Count the draws and the whole run is accounted for: two to place the spring, which lands at column 5, row 3, and one per step of the walk. Thirty-two draws in all, each one pulled from the fixed sequence that seed 5 unrolls.
Read the map the way you read last chapter's, but notice who drew it. Thirty flooded steps produced only sixteen water cells, so the walk crossed its own path fourteen times; the dense southwest bulge is where it lingered.
Nobody chose those cells. Nobody can choose them: change the seed and a different pond grows in a different corner. What you chose is the number 5, and that is the only authorship left.
Generate builds its own rng, uses it, and lets it die. That
is a discipline, not an accident: a generator is a consumable stream, and any code
that shares one with somebody else no longer controls what it will draw. The worked
failure below is what happens when this rule bends.
The same seed twice
The claim on the table is strong. Same seed, same world: not similar, not statistically alike, but equal in every one of 96 cells.
Strong claims get tested by machine, not by squinting. Render already
flattens a whole grid into one string, and Go compares strings with ==,
byte by byte. Build the world twice, build a rival world from seed 6, and let three
comparisons speak.
// cmd/worldd/main.go — the body of main grows a proof
func main() {
const seed = 5
fmt.Println("worldd", version, "seed", seed)
first := sim.Generate(12, 8, seed)
second := sim.Generate(12, 8, seed)
other := sim.Generate(12, 8, seed+1)
fmt.Print(first.Render())
fmt.Println("same seed, same world: ", first.Render() == second.Render())
fmt.Println("seed 6, same world: ", first.Render() == other.Render())
differ := 0
for y := 0; y < first.H; y++ {
for x := 0; x < first.W; x++ {
c := sim.Coord{X: x, Y: y}
if first.At(c) != other.At(c) {
differ++
}
}
}
fmt.Println("cells where seed 6 disagrees:", differ)
}
$ go run ./cmd/worldd
worldd 0.0.1 seed 5
############
#..........#
#..........#
#.~~~~~....#
#.~~~~~~...#
#...~~~~...#
#...~......#
############
same seed, same world: true
seed 6, same world: false
cells where seed 6 disagrees: 29
The first true is the sentence this volume is built on, printed by the
program itself: two separate calls, two separate grids, two separate ponds grown step
by step, and not one byte of difference between them.
The seed-6 lines calibrate how much that means. One seed away, 29 cells disagree, because seed 6's first draws differ and every step of its walk compounds the divergence.
Nearby seeds are not nearby worlds. Each seed is a doorway into a stretch of the sequence unrelated, for any purpose you care about, to its neighbor's.
Why deterministic generation works
The guarantee is airtight because nothing in the chain has anywhere to hide state you
did not supply. A PRNG's next output is computed from its current state alone; the
starting state is computed from the seed alone; and Generate consumes draws
in an order fixed by its own code, two for the spring and then one per step.
Same seed, same first draw. Same first draw, same spring. Same spring and same second draw, same first step. Each equality forces the next, all the way down the walk, and the identical maps are that induction made visible.
Determinism belongs to the generator itself; the craft in PCG's design went into making a fully determined sequence look lawless.
This is also why the seed deserves to be called the world's name, the one metaphor this chapter needs. The 96-cell map takes 96 cells to write down, but "seed 5" reproduces it from eight characters. Anything grown from draws is carried by the number the draws came from.
Report a bug as "seed 5, tick 300" and anyone can stand exactly where you stood. Keep a seed in a config file and a server can be rebuilt from bare metal into the same valley.
The same reasoning convicts the tempting alternatives. Seeding from the clock produces a world named by the nanosecond you happened to press enter, a name nobody can use twice.
Go's own package-level functions, rand.IntN and family called without a
generator, are worse in a quieter way: math/rand/v2 seeds them unpredictably
at startup, on purpose, and offers no way to re-seed.
They exist for programs that want dice, and a simulation is not allowed to want dice. It wants a ledger of decisions it can replay. Every stochastic system in this book gets draws from a generator whose seed is written down.
Here is the version of this chapter that almost got written. It seemed tidier to
build the generator once in main and pass it in, so that callers could
share one source of randomness:
// The signature that seemed cleaner:
func Generate(w, h int, rng *rand.Rand) *Grid { ... }
// main, seeding once and generating twice:
rng := rand.New(rand.NewPCG(5, 0))
first := sim.Generate(12, 8, rng)
second := sim.Generate(12, 8, rng)
fmt.Println("same seed, same world: ", first.Render() == second.Render())
fmt.Print(second.Render())
$ go run ./cmd/worldd
worldd 0.0.1 seed 5
same seed, same world: false
############
#.......~~.#
#........~~#
#........~~#
#........~~#
#.....~~~~~#
#...~~~~...#
############
The proof line reads false, and the printed second map is a pond seed 5
never grew. Reason from the symptom.
Both calls saw the same generator. The worlds differ because a generator is not a value; it is a stream with a position.
The first Generate consumed draws one through thirty-two. The second
began at draw thirty-three, mid-sequence, and grew its pond from numbers that belong
to no seed anyone will ever type.
The world it built is an orphan: real, valid, and unreproducible, exactly the kind of world this chapter exists to forbid.
The fix is the code you already have. A world's generator is born from the seed inside
Generate and dies there, making draw one of world A and draw one of
world B the same draw.
When a stream is shared across systems, the sharing is an explicit design with the draw order pinned. It is never an accident of a convenient signature.
Checkpoint
- Hand-crank a linear congruential generator, (5s + 3) mod 16 from any seed, and predict its output before a program confirms it.
- Explain why a PRNG's sequence is fixed the moment it is seeded, and why that makes "random" and "repeatable" compatible instead of contradictory.
- Say what
rand.NewPCG(seed, 0)andrng.IntN(n)each contribute, and account for every drawGenerateconsumes, in order. - Prove same-seed determinism the mechanical way: two generated grids compared
as strings with
==, no squinting involved. - Given a "same seed, different world" symptom, check whether two consumers are drinking from one shared stream before suspecting the generator itself.
- Say why a simulation must not touch the auto-seeded package-level
randfunctions, even though they compile fine.
Exercise 1 — the seed safari. Loop seeds 1 through 12, generating and rendering each. Every one of those worlds already existed before you ran the loop; pick the one you would want to live in, and note its number.
A three-line loop in main: for seed := uint64(1); seed <=
12; seed++, print the seed, print the render. You will find real variety:
seed 3 grows a two-lobed pond with a channel, seed 4's water hugs the western
wall like a river, seed 7 puddles in the northwest corner. Run the loop twice
and the safari itself repeats exactly, which is the chapter's proof at gallery
scale. Whatever number you picked, write it down; a favorite seed is a world you
can come back to.
Exercise 2 — break the toy. In the stage 1 generator, change the increment from 3 to 4 and run seeds 7 and 8 again. The period was 16; what is it now, and what does the answer say about how carefully real generator constants are chosen?
Seed 7 prints 7 7 7 7 …: since 5·7 + 4 = 39 and 39 mod 16
is 7, the state is a fixed point and the period collapses to 1. Seed 8 fares
barely better, cycling 12 0 4 8 forever, period 4. One nudged
constant turned a full-lap generator into a stopped clock, and which disaster
you get depends on the seed. The constants in a real PRNG are not adjustable
knobs; they are load-bearing numbers with proofs attached, and the library's job
is to have chosen them so you never think about it.
Exercise 3 — the second word. NewPCG takes
two 64-bit words and Generate passes 0 as the second. Change that 0
to 1, keep seed 5, and run the proof again. Is it still the same world?
The two grids still match each other, so the proof line stays
true; what changed is which world seed 5 means. Compare the map
against the one printed above and it is a different pond: a lobe stretched
across the north with one dry cell trapped inside it, and a second patch down
the western side. Both words are part of the starting state, so the world's
true name is really the pair (5, 0), and terrain's word is a constant 0
precisely so that one number stays a complete name. Changing it does not hand
you another seed's world; it hands you another stream of the same seed, which
is the mechanism the stream convention rests on. Put the 0 back before moving
on, and the pond returns to the one this chapter grew.