Brick & Mortar

Devlog · Post 6 of 27

Two PCs, one simulation - lockstep, determinism and a 64-bit fingerprint

· 4 min read

unrealmultiplayerdeterminismtesting

A hillside clash in low golden sun, with tanks and infantry scattered across the grass.

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.

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.

A battle from the real simulation. The same take, re-shot at normal speed, logged exactly the same census.

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

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.