Prove It Twice
Same seed, same bytes
Six hundred ticks now produces tens of thousands of bytes of history, and none of them are being checked. The rule is exact: same seed, same tick count, same bytes: two runs of this world produce event logs that are equal byte for byte, and a single differing byte is a bug in the simulation, never noise.
Back when the world was 96 cells of rock and water, proving that a seed determined it
took one line: generate the grid twice, compare the two renderings with ==,
print true. That proof was honest and it is now badly out of date.
The world has entities with identities, a tick counter, laws that draw from a seeded stream every tick, moves that get refused by the ground, a spring that floods a cell every forty ticks, and a log recording all of it one line at a time.
The gap matters more than it looks. Suppose a walker ends up standing in water, and the log says it happened at tick 4,102 of seed 5. In a deterministic world you rerun seed 5, stop at tick 4,102, and stare at the same walker in the same puddle as often as you need.
Otherwise you have a story about something that happened once to somebody's process. No tolerance, no "mostly the same", no fields exempted for being unimportant. The test has to catch one differing byte.
The _test.go file
Go has testing built into the toolchain, so there is no library to choose and nothing
to install. Put a file whose name ends in _test.go beside the code it
tests; write functions named TestSomething taking one argument of type
*testing.T; run go test. Those files are invisible to
go build, so test code never ships inside worldd, and
because this one declares package sim rather than an external test
package it can reach unexported names in the package it is testing.
Start with chapter 4's generator claim, the smallest true thing this test file needs to defend. It fits in nine lines and its job here is to introduce the machinery.
// internal/sim/gen_test.go
package sim
import "testing"
func TestGenerateRepeatsItself(t *testing.T) {
first := Generate(12, 8, 5).Render()
second := Generate(12, 8, 5).Render()
if first != second {
t.Fatalf("seed 5 grew two different worlds:\n%s\n%s", first, second)
}
}
$ go test -v -run TestGenerate ./internal/sim/ (elapsed times measured on the author's machine; yours will differ) === RUN TestGenerateRepeatsItself --- PASS: TestGenerateRepeatsItself (0.00s) PASS ok theworld/internal/sim 0.002s
$ go test ./...
? theworld/cmd/worldd [no test files]
ok theworld/internal/sim 0.010s
A test reports failure by calling a method on t, and never by returning
a value or panicking. t.Fatalf formats a message, marks the test failed,
and stops that test function there; its sibling t.Errorf marks the
failure and keeps going, for when several independent checks each deserve reporting.
Nothing prints on success unless you ask, which is what -v does. The
message matters as much as the check: it is written for the version of you who reads
it at midnight with no memory of this file, so it says what was compared and shows
both sides.
The second command runs every package in the module. cmd/worldd has no
tests, and Go says so plainly instead of pretending it passed. The tool is also
aggressive about not repeating work: run go test twice without changing
anything and the second run answers ok theworld/internal/sim (cached)
in no time at all, because the result of a test whose code and inputs are unchanged
cannot have changed either. That is a convenience most of the time and a trap when
you are chasing something that only fails sometimes, so a chapter about
nondeterminism keeps -count=1 within reach.
-v prints every test as it runs, along with anything
t.Logf wrote. -run PATTERN selects test functions by
regular expression, so -run TestSameSeed runs one of them.
-count=1 disables the result cache and forces a real run.
-count=N runs the whole selection N times over, which is how you make
an intermittent failure show itself. Full reference:
pkg.go.dev/cmd/go#hdr-Testing_flags.
The run moves into sim
The generator was easy to test because it is a function you can call. The world is
not: setting it up, spawning entities, opening a log, and running the loop all live in
main, and main is the one function no test can call. That is
not a testing quirk to work around. It is a design problem the test just exposed, and
the fix improves the program on its own merits: everything about what a run
is moves into the sim package behind one call, and
main keeps what only a command-line program needs, which is flags,
signals, and last chapter's goroutine and stop channel.
// internal/sim/run.go
package sim
import (
"fmt"
"time"
)
// Config is everything one run of the world needs to know. Every
// field is a decision the caller makes; nothing here is read from the
// machine.
type Config struct {
Seed uint64 // names the world and every draw inside it
Ticks uint64 // how many ticks to take before stopping
Log string // where the event log is written
Paced bool // deliver ticks at TickRate, or as fast as they compute
}
// Run plays one world from beginning to end and writes its history to
// cfg.Log. It returns when the world has taken cfg.Ticks ticks, or
// when stop is closed, whichever happens first.
func Run(cfg Config, stop <-chan struct{}) error {
lg, err := OpenLog(cfg.Log)
if err != nil {
return err
}
defer lg.Close()
if err := lg.Append(Event{Kind: "start", What: fmt.Sprintf("seed %d", cfg.Seed)}); err != nil {
return err
}
w := NewWorld(Generate(12, 8, cfg.Seed), cfg.Seed)
w.Record(lg)
for _, s := range []struct {
kind EntityKind
at Coord
}{
{Shrub, Coord{X: 2, Y: 5}},
{Walker, Coord{X: 10, Y: 6}},
{Walker, Coord{X: 2, Y: 2}},
} {
if _, err := w.Spawn(s.kind, s.at); err != nil {
return err
}
}
var beat <-chan time.Time
if cfg.Paced {
t := time.NewTicker(TickDuration)
defer t.Stop()
beat = t.C
}
reason := "ticks complete"
for w.Tick() < cfg.Ticks {
if beat != nil {
<-beat
}
select {
case <-stop:
reason = "stopped"
default:
}
if reason == "stopped" {
break
}
if err := w.Step(); err != nil {
return err
}
if err := w.Faulted(); err != nil {
return err
}
}
return lg.Append(Event{Tick: w.Tick(), Kind: "stop", What: reason})
}
// cmd/worldd/main.go — flags, signals, and the goroutine
func main() {
seed := flag.Uint64("seed", 5, "the number that names this world")
ticks := flag.Uint64("ticks", 600, "how many ticks to take")
logPath := flag.String("log", "world.jsonl", "where to write the event log")
fast := flag.Bool("fast", false, "deliver ticks as fast as they compute")
flag.Parse()
cfg := sim.Config{Seed: *seed, Ticks: *ticks, Log: *logPath, Paced: !*fast}
fmt.Printf("worldd %s seed %d ticks %d -> %s\n", version, cfg.Seed, cfg.Ticks, cfg.Log)
stop := make(chan struct{})
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
go func() {
<-sig
close(stop)
}()
done := make(chan error, 1)
go func() { done <- sim.Run(cfg, stop) }()
if err := <-done; err != nil {
fmt.Fprintln(os.Stderr, "worldd:", err)
os.Exit(1)
}
fmt.Println("worldd: the world stopped cleanly")
}
$ go run ./cmd/worldd -fast -ticks 600 -log a.jsonl
worldd 0.0.1 seed 5 ticks 600 -> a.jsonl
worldd: the world stopped cleanly
Two decisions in Config are deliberate. The seed and the
tick count are inputs a caller supplies, so a run is fully described by a value you
can print, store, and paste into a bug report. And Paced separates the
delivery of ticks from their content, which is chapter 7's rule finally paying a
dividend. Paced, the ticker hands out a tick every hundred milliseconds and 600
ticks take a minute; with -fast the same 600 ticks land as quickly as
the processor can compute them, and the resulting world is identical in every
respect, because nothing inside the simulation has any way to ask how long a tick
took. A minute of world, verifiable in milliseconds.
Run ends by writing a stop line naming why it stopped. Two
runs that both finished their tick count agree on that line; a run cut short by a
signal records the tick it actually reached, which is a real difference in what
happened and belongs in the history.
Now the test can hold a whole world. It runs one, runs another, and compares every byte of the two files.
// internal/sim/replay_test.go
package sim
import (
"bytes"
"os"
"path/filepath"
"testing"
)
// history plays one world into a file of its own and returns every
// byte the run wrote.
func history(t *testing.T, cfg Config) []byte {
t.Helper()
cfg.Log = filepath.Join(t.TempDir(), "world.jsonl")
if err := Run(cfg, nil); err != nil {
t.Fatalf("run seed %d: %v", cfg.Seed, err)
}
b, err := os.ReadFile(cfg.Log)
if err != nil {
t.Fatalf("read log: %v", err)
}
return b
}
// firstDiff reports the number of the first line on which two
// histories disagree, and both versions of it.
func firstDiff(a, b []byte) (int, string, string) {
x := bytes.Split(a, []byte("\n"))
y := bytes.Split(b, []byte("\n"))
for i := 0; i < len(x) && i < len(y); i++ {
if !bytes.Equal(x[i], y[i]) {
return i + 1, string(x[i]), string(y[i])
}
}
return min(len(x), len(y)) + 1, "", ""
}
// body drops the start line, which names the seed and so is expected
// to differ between two worlds.
func body(b []byte) []byte {
_, rest, _ := bytes.Cut(b, []byte("\n"))
return rest
}
func TestSameSeedSameHistory(t *testing.T) {
cfg := Config{Seed: 5, Ticks: 600}
first := history(t, cfg)
second := history(t, cfg)
if len(first) == 0 {
t.Fatal("the run wrote no history at all")
}
if !bytes.Equal(first, second) {
n, a, b := firstDiff(first, second)
t.Fatalf("two runs of seed %d disagree at line %d\n run 1: %s\n run 2: %s",
cfg.Seed, n, a, b)
}
t.Logf("seed %d: %d bytes of history, identical twice", cfg.Seed, len(first))
}
func TestDifferentSeedsDifferentHistories(t *testing.T) {
five := history(t, Config{Seed: 5, Ticks: 600})
nine := history(t, Config{Seed: 9, Ticks: 600})
if bytes.Equal(body(five), body(nine)) {
t.Fatal("seeds 5 and 9 wrote the same history; the seed is being ignored")
}
n, _, _ := firstDiff(body(five), body(nine))
t.Logf("seeds 5 and 9 part company at event %d", n)
}
$ go test -v ./internal/sim/ (elapsed times measured on the author's machine; yours will differ) === RUN TestGenerateRepeatsItself --- PASS: TestGenerateRepeatsItself (0.00s) === RUN TestSameSeedSameHistory replay_test.go:59: seed 5: 65804 bytes of history, identical twice --- PASS: TestSameSeedSameHistory (0.00s) === RUN TestDifferentSeedsDifferentHistories replay_test.go:70: seeds 5 and 9 part company at event 4 --- PASS: TestDifferentSeedsDifferentHistories (0.00s) PASS ok theworld/internal/sim 0.010s
Sixty-five thousand bytes of world, written twice, with no byte out of place. Three
small decisions hold that result up. t.TempDir() hands back a fresh
directory per call, which the log's design demands: the file is opened for append,
so two runs pointed at one path would stack their histories into one file and the
test would compare something other than what it claims. t.Helper()
marks history as plumbing, so a failure inside it is reported at the
calling line in the test. And the second test stops the first from being a fraud: a
Run that ignored its seed, or wrote nothing at all, would sail through
an equality check. Something has to insist that different seeds still produce
different worlds, and that they diverge at the fourth event, the first move either
walker makes.
The test proves the property inside one process. The claim is bigger than that, so make it again the hard way, with two separate executions of a compiled binary. Delete the logs first, since an append-only file happily survives its own program.
go build -o worldd ./cmd/worldd
rm -f a.jsonl b.jsonl
./worldd -fast -seed 5 -ticks 600 -log a.jsonl
./worldd -fast -seed 5 -ticks 600 -log b.jsonl
diff -u a.jsonl b.jsonl && echo "identical"
$ ./worldd -fast -seed 5 -ticks 600 -log a.jsonl; ./worldd -fast -seed 5 -ticks 600 -log b.jsonl
worldd 0.0.1 seed 5 ticks 600 -> a.jsonl
worldd: the world stopped cleanly
worldd 0.0.1 seed 5 ticks 600 -> b.jsonl
worldd: the world stopped cleanly
$ diff -u a.jsonl b.jsonl && echo "identical"; wc -l a.jsonl b.jsonl
identical
953 a.jsonl
953 b.jsonl
1906 total
$ sha256sum a.jsonl b.jsonl
4cb81462cf8875fc307dceacba66f8e51fc7b8777866edaf3c271ea06c5d5139 a.jsonl
4cb81462cf8875fc307dceacba66f8e51fc7b8777866edaf3c271ea06c5d5139 b.jsonl
diff printing nothing is the whole result: 953 lines apiece and no line
that differs, from two processes that shared no memory and ran at different moments
on a machine doing other things in between. The checksums say it in one number each.
Two tools, two definitions of "the same", one verdict.
Nondeterminism bug
A passing test proves nothing until you have watched it fail for the right reason. So break the world the way it actually gets broken, with a change that is faster, simpler, and correct-looking.
wander asks for Roster() every tick, and
Roster allocates a fresh slice and fills it by walking IDs. Ten times
a second, forever, for a list the world already holds in a map. Deleting the
middleman is a one-word edit.
// internal/sim/tick.go — the "obvious" improvement
func (w *World) wander() error {
for _, e := range w.ents { // was: for _, e := range w.Roster()
if e.Kind != Walker {
continue
}
d := steps[w.rng.IntN(len(steps))]
err := w.Move(e.ID, e.At.Offset(d.X, d.Y))
// ... unchanged
}
return nil
}
Run the server and nothing looks wrong. It starts, it ticks, it stops cleanly, it writes a log full of plausible walkers taking plausible steps. Run the test:
$ go test ./internal/sim/ (a broken world fails differently every time; these two runs are the author's) --- FAIL: TestSameSeedSameHistory (0.00s) replay_test.go:56: two runs of seed 5 disagree at line 5 run 1: {"t":1,"ev":"move","id":3,"from":{"x":2,"y":2},"to":{"x":2,"y":1}} run 2: {"t":1,"ev":"move","id":2,"from":{"x":10,"y":6},"to":{"x":10,"y":5}} FAIL FAIL theworld/internal/sim 0.011s FAIL
$ go test -count=1 ./internal/sim/
--- FAIL: TestSameSeedSameHistory (0.00s)
replay_test.go:56: two runs of seed 5 disagree at line 13
run 1: {"t":6,"ev":"move","id":2,"from":{"x":9,"y":4},"to":{"x":10,"y":4}}
run 2: {"t":6,"ev":"move","id":2,"from":{"x":9,"y":4},"to":{"x":8,"y":4}}
FAIL
FAIL theworld/internal/sim 0.011s
FAIL
Read the first failure. At tick 1 one run moved walker 3 and the other moved walker
2 first: the same two moves, in opposite order. Go randomizes the starting point of
every range over a map, on purpose, precisely so that nobody can come
to depend on an order the language does not promise. The second failure shows what
that costs once the order touches the seeded stream. Both runs move walker 2 out of
{9 4} at tick 6, and one sends it east while the other sends it west, because
whichever walker is served first takes the next draw from w.rng, and
after one swap the two runs are drawing from the same sequence in different places
forever. Neither failure will be the failure you get: a broken world lands somewhere
new every time, which is the entire complaint.
The size of the damage is easier to see from the shell, comparing two runs of the broken binary. What follows is one such pair, captured once; yours will land elsewhere, and that is the point:
$ diff a.jsonl b.jsonl | head -4; wc -l a.jsonl b.jsonl (one roll of a broken binary; no two are alike) 8d7 < {"t":3,"ev":"move","id":3,"from":{"x":1,"y":2},"to":{"x":0,"y":2}} 9a9 > {"t":3,"ev":"move","id":3,"from":{"x":1,"y":2},"to":{"x":0,"y":2}} 977 a.jsonl 957 b.jsonl 1934 total
It opens as one line displaced by a single position and ends as two different
worlds: 977 events against 957 on this roll, and of those, 815 lines of the first
history appear nowhere in the second. The correct binary writes 953 lines for seed
5, every time; neither of these is that number, and running the pair again gives two
more numbers that are neither. Look closely and the ground itself survives intact —
the spring floods the same fifteen squares in both runs, because two walkers still
draw twice per tick and the pond's own draws land in the same places. It is the
walkers that come apart, and once they are on different cells the two logs are
describing different afternoons. Nothing about that is visible in either
run on its own, and no amount of reading the code would tell you which of the two is
the real seed 5. Putting Roster() back fixes it, and the cost of the
fix is one small allocation per tick, which is a bargain for the only property that
makes any of this history worth keeping.
The general lesson is bigger than maps. Any decision the world makes by consulting something the seed does not control (map order, wall-clock time, a goroutine that happened to finish first) leaks the machine's mood into the world's history. The replay test does not care which. It reports the first line where the world stopped agreeing with itself, and you go and find out why.
Determinism boundary
Determinism at this scale is not luck, and not something you sprinkle on later. It holds because every input to a decision inside the world passes through a boundary you drew, and only two things are allowed across: the seed and the tick count. Draws come from a generator built from the seed, in a fixed order. Entities are served in ID order, which the world assigns. Sim time comes from a counter the world increments. The laws consult none of the machine's ambient facts, because the code never asks for one. Same two inputs, same sequence of decisions, and the log records that sequence in the order it happened.
Figure 10.1: the replay test guards this boundary. Anything that gets across it on the left without being an input shows up on the right as a differing line.
That property is the contract every larger part of this book signs. A faithful snapshot can be checked by replaying the log from the beginning and comparing the rebuilt state, byte for byte, which only means anything if replaying is exact.
Server and client agreement uses the same property: replay the seed and the tick range on both sides, then compare what happened. A result you cannot reproduce is not a result, and "run it again with seed 5" is the difference between an experiment and an anecdote.
Determinism is not a feature you add once; it is a property one careless line can destroy at any point, and it destroys quietly: no crash, no error, just a world that answers the same question differently on Tuesday. The test runs before everything else because it is cheap, exact, and the witness that notices.
Every output printed in these pages comes from actually running the code in a container, and the ones from deterministic systems get checked byte for byte against what the page claims. That is the same machine you just built, pointed at the book instead of at the world: a fixed seed, a run, and a comparison with no tolerance in it. Where a number cannot work that way, because a language model wrote it or because it measures your hardware, the page says so and tells you your run will differ.
Checkpoint
- Write a
_test.gofile in the same package as the code it tests, and say why test files never end up inside theworlddbinary. - Choose between
t.Fatalfandt.Errorf, uset.Helper()so failures point at the calling line, and uset.TempDir()so an append-only log never carries over between runs. - Restructure a program so a test can reach it, moving a run behind
sim.Run(cfg, stop)and leaving flags and signals inmain. - Explain why
-fastand the paced ticker produce identical histories, and use that to verify a minute of sim time in milliseconds. - Prove byte-identical replay two ways:
go testcomparing two in-process runs, anddiffplussha256sumover the logs of two separate binaries. - Recognize a nondeterminism bug from a diff that starts with two swapped lines and ends with two different worlds, and name the usual culprits.
Exercise 1: leak the clock on purpose. Add a
Wall string field to Event with the tag
json:"wall,omitempty", and set it on the start event
from time.Now().Format(time.RFC3339Nano). Predict what the test says
before running it.
It fails on line 1, and the message hands you the culprit with no investigation required:
$ go test ./internal/sim/
--- FAIL: TestSameSeedSameHistory (0.00s)
replay_test.go:56: two runs of seed 5 disagree at line 1
run 1: {"t":0,"ev":"start","what":"seed 5","wall":"2026-08-24T17:29:29.887000744-05:00"}
run 2: {"t":0,"ev":"start","what":"seed 5","wall":"2026-08-24T17:29:29.888931288-05:00"}
FAIL
Under two milliseconds apart, and the histories are already different files. The answer is not to forbid wall time but to keep it out of the event stream: an operator's question about when a run happened belongs in the operator's own logs, where nothing replays. Take the field back out before continuing.
Exercise 2: stop it by hand. Run
./worldd -seed 5 -ticks 600 -log c.jsonl without
-fast, press Ctrl-C after a couple of seconds, then look at the tail
of the log. Is this a determinism bug?
A run interrupted after two seconds ends like this, with the tick reached depending on exactly when your finger landed:
$ tail -2 c.jsonl; wc -l < c.jsonl
{"t":22,"ev":"move","id":3,"from":{"x":3,"y":1},"to":{"x":4,"y":1}}
{"t":22,"ev":"stop","what":"stopped"}
43
Not a bug. The signal is an input from outside the world, as real as the seed,
and the log records a run that stopped at tick 22 instead of 600. A bug would be
the first 22 ticks disagreeing with the first 22 ticks of a full run, and they
do not: head -42 of the uninterrupted log matches this file exactly.
So the test fixes Ticks instead of stopping by signal, because a
test controls its inputs and a keystroke is not one of them.
Exercise 3: five worlds instead of one. Write a
table-driven test that replays seeds 5, 9, 10, 11 and 12 for 300 ticks each, using
t.Run so every seed reports separately. Then run it with
-count=5.
t.Run(name, func(t *testing.T){...}) starts a subtest with its own
t, so one failing seed names itself instead of hiding inside a
failure about "the seeds".
$ go test -v -run TestManySeeds ./internal/sim/
=== RUN TestManySeedsReplay
=== RUN TestManySeedsReplay/seed5
=== RUN TestManySeedsReplay/seed9
=== RUN TestManySeedsReplay/seed10
=== RUN TestManySeedsReplay/seed11
=== RUN TestManySeedsReplay/seed12
--- PASS: TestManySeedsReplay (0.01s)
--- PASS: TestManySeedsReplay/seed5 (0.00s)
--- PASS: TestManySeedsReplay/seed9 (0.00s)
--- PASS: TestManySeedsReplay/seed10 (0.00s)
--- PASS: TestManySeedsReplay/seed11 (0.00s)
--- PASS: TestManySeedsReplay/seed12 (0.00s)
PASS
ok theworld/internal/sim 0.012s
Add seeds 6, 7 and 8 to the table and the subtest naming pays for itself immediately:
$ go test -v -run TestManySeeds ./internal/sim/ 2>&1 | grep -E "FAIL|seeds_test"
seeds_test.go:13: run seed 6: spawn: walker at {2 2} is water: terrain refuses the entity
seeds_test.go:13: run seed 7: spawn: walker at {2 2} is water: terrain refuses the entity
seeds_test.go:13: run seed 8: spawn: walker at {10 6} is water: terrain refuses the entity
--- FAIL: TestManySeedsReplay (0.01s)
--- FAIL: TestManySeedsReplay/seed6 (0.00s)
--- FAIL: TestManySeedsReplay/seed7 (0.00s)
--- FAIL: TestManySeedsReplay/seed8 (0.00s)
FAIL
Not a replay failure at all, and not the same failure three times either. The
line number is the giveaway: 13 is where the subtest calls history,
and history is a t.Helper(), so a failure raised deep
inside Run is reported against the line you would actually go and
read. Seeds 6 and 7 put water on {2 2}, so it is the second walker
Run spawns that the ground refuses; seed 8 floods {10 6} instead
and stops the first one. Three of the eight seeds a reasonable person would try
cannot even reach tick 1, because two starting positions are written into
Run as literals and no terrain was ever asked to agree with them.
A test that runs many worlds finds the ones your hard-coded starting positions
were never designed for, which is a real defect in Run waiting for
a real fix.