Devlog · Post 6 of 27
Two PCs, one simulation - lockstep, determinism and a 64-bit fingerprint
unrealmultiplayerdeterminismtesting

A real-time strategy game can have a hundred units fighting. Sending all of their positions over the internet would be heavy. So strategy games usually do something cleverer, called lockstep: every PC runs the whole simulation, and the only thing that travels is the players' orders.
It only works if the simulation is deterministic: given the same orders on the same tick, every PC must compute exactly the same result, bit for bit. If one PC's missile turns half a degree differently, a cell boundary falls differently, a unit dies one tick earlier, and a minute later the two players are watching different games.
The easy half
Our simulation already stepped in fixed 50 millisecond ticks, and orders were already commands. The pieces were: one message type for "what a click becomes", a turn packet that carries every seat's commands for a tick, and a short input delay so the next turn has always arrived.
The hard half: the audit
Before writing network code we asked Claude to audit the whole simulation for every place two PCs could disagree. It came back with sixteen, in four groups. They are worth reading, because every one is an ordinary line of code.
- Dice that bypass the seed. Civilians walking around town used the process's own random numbers, seeded from the clock. Five places. A different walk changes who blocks whose path, and the difference spreads.
- The picture feeding the battle. Shells started from the drawn barrel, whose rotation is smoothed every rendered frame. At 30 and at 144 frames a second the barrel is in a slightly different place, so the shell is too.
- Maths that rounds differently. Sine, cosine and power call the Windows C runtime, which picks a different code path depending on the CPU. The last bit can differ between two machines, and a rounding to a whole cell turns that bit into a different cell. Six places: a homing turn, a paradrop landing, a storm bolt, the AI's probe cells, and others.
- The build itself. The packaged game was compiled with fast floating-point maths and our editor tests with precise maths. Online, everyone runs the same exe, but our tests would not have predicted what shipped.
The fixes were small: draw from the seeded stream, use the engine's own SinCos and Atan2, a four-entry table instead of a power function, compile both targets the same way. We also wrote down six house rules for simulation code. The first one is the important one: the sim reads only sim state. Never a transform, the clock, the camera, the local seat or a user setting. A grep lint, scripts/detlint.py, runs with every build.
The fingerprint
Every ten ticks (half a second) each PC computes a 64-bit hash of the entire game state: the tick, the random generator, every actor's id, position bits, health and order, every seat's credits and queues, projectiles, the ore grid. The hash rides along on the turn packets. If two PCs disagree, the match stops with a message, and we know the tick.
In tests the hash is taken on every tick, written to a log, and two logs are compared by a script. Our test pairs: the same exe twice (30 minutes, identical), one run at 8x speed against another at 64x headless (identical, with a live player's 326 to 348 commands replayed), and two packaged builds against each other.
The first real match
On 25 September the owner played the first online match with a friend, on two PCs. It stayed in sync to the end: no desync line in the host's logs, and 4,080 checks passed. It was the day the project turned from a single-player toy into a game two people could play together.
We have not yet compared logs tick by tick between two different machines, and we say so in our notes. The stress test we planned (eight AIs on the biggest map for an hour of sim, ten seeds) is still to be run.
What we would do differently
- Audit for determinism before writing the second system. Every feature after the first is a chance to add a clock read.
- Test the build you ship. Our best catch was the compiler setting, found by reading the build tool's source, not by a test.
- Keep the picture and the simulation apart from the first day. Effects, audio and interface must never call into the sim.
Try it yourself
If you are making any game you might ever want online, write one test early: run two copies, log a hash every tick, compare. It takes an afternoon and will tell you whether your project is ever going to work.
Next: The documents ate the budget - tokens, a ledger and a compactor (25 September 2026)
Start from day oneAll 27 posts
Written afterwards from our project notes and commit history; dated to when the work happened.


