← All posts

Determinism is a decision you make first

The first commit has no weapons, no inventory, no bots and no level. It is a workspace skeleton, a clock, and a set of prohibitions.

That ordering is deliberate, and it is the one structural decision in this project that genuinely could not have been made later.

Three crates, and one of them cannot see the engine

The simulation lives in a crate with no game engine dependency at all. The server is a headless binary with no rendering compiled into it. The client has the engine and does presentation only.

The boundary is not a convention or a code review habit. There is a test that reads the dependency manifests and fails if the simulation crate ever gains an engine dependency. Mechanical enforcement, because a boundary maintained by discipline is a boundary that erodes the first week someone is in a hurry.

The clock banks its leftovers

Sixty ticks a second, accumulated in integer nanoseconds, and only whole ticks ever run. Leftover time is banked and never reaches simulation state.

Overload is capped at eight ticks per update and the excess is discarded rather than queued. A server that falls behind and then tries to catch up by running a hundred ticks in one update gets slower, which makes it fall further behind. That spiral is a well-known way to turn a brief hitch into a dead server, and the cap is the whole fix.

What the simulation is not allowed to do

This is the part that reads as paranoia and is not.

The random number generator is seeded and threaded explicitly through the simulation state. No global generator, no thread-local one. The convenient crate that would provide those is deliberately not a dependency, because the convenience is precisely the hazard: a global generator means any function anywhere can draw from it, and two machines drawing in a different order diverge.

Floats are hashed by bit pattern. And three things are on a lint deny-list that fails the build:

  • Trigonometry. The platform's maths library is not guaranteed identical
  • across machines.

  • Hash maps and hash sets. Iteration order varies, and anything that
  • iterates them feeds that order into the result.

  • Reading the wall clock. A simulation that asks what time it is has
  • stopped being a function of its inputs.

Each of those is fine in ordinary code. Each of them, inside a simulation that two machines must run to identical results, is a bug that will surface as "desync sometimes, for some players, unreproducibly" months later.

Why this cannot be retrofitted

The determinism harness runs a thousand ticks and hashes the result to a single number recorded in the source. Any change to simulation behaviour moves that number, on purpose, and the new value is recorded in the same commit.

The reason this goes first is that determinism is not a feature you add. It is a property every line of simulation code either has or breaks. Adding the harness after ten thousand lines exist means auditing ten thousand lines for three categories of subtle violation, with no way to tell which of them matters until you find one. Adding it at line zero means every subsequent violation is caught by the build, on the commit that introduced it, by someone who still remembers writing it.

An honest note about this commit

It had never been compiled when it was written. The machine available at the time had no working toolchain, so every numeric assertion in it was verified by recomputing it by hand against floating-point semantics, and the engine APIs were checked against documentation.

The first build was expected to be red, and was. The golden hash was a placeholder that fails by design and prints the correct value to record.

Writing a thousand lines of untested code is not a practice worth recommending. It is what the situation allowed, and the mitigation was to make the failures loud and predicted rather than to hope.