← Engineering log

25 September 2026 · 5 min read

Scores the server doesn't take on trust

The arcade's leaderboards don't believe the number the browser sends. They replay the whole run, on the server, and keep the score they get.

A leaderboard that accepts { score: 4210 } from the browser is a leaderboard for anyone who can open DevTools. For a while the arcade did what most do: it rejected the impossible (a score higher than a run of that length could reach) and trusted the rest. That stops the lazy cheat and nothing else. The plausible fake, a believable score typed in by hand, went straight through.

So the arcade stopped sending scores. It sends the run instead, and the server plays it again.

Send the input, not the result

While you play Asteroid Run, the browser records your steering: three whole numbers (the keys you're holding, and where the pointer is), 30 times a second. When the run ends, that recording goes to the server with the score you saw. The server feeds the recording into the same simulation the game uses and computes the score itself. The number you claimed is ignored; the replayed one is what goes on the board. Claiming 99,999 stores your real score.

That only works if the game is deterministic: the same input must produce exactly the same run, on a phone's browser and on a server in a data centre. Getting there took more care than the replay itself.

Making a game deterministic

  • A fixed timestep. The simulation always advances in steps of 1/120 of a second, whatever the frame rate. A 144 Hz monitor and a struggling phone run the same steps; they just render them differently. Even the slow-motion shot when you leave a planet's orbit changes how fast steps are consumed, never their size.
  • One source of randomness. Every rock, fragment and power-up comes from a seeded generator (mulberry32), never Math.random.
  • No Math.sin. The JavaScript spec lets each engine approximate trigonometry its own way, so Chrome, Safari and Node can disagree in the last bits. In a game where a rock clipping your wing ends the run, the last bit is enough to diverge. Free flight carries its own polynomial sin, cos and atan2, built only from addition and multiplication, which IEEE 754 makes identical everywhere.

A tape small enough to post

The recording is a list of samples, packed into a short string:

[keys held, pointer x, pointer y] every 4 steps (30 a second)
  → the pointer stored as the change from the last sample
  → a run of identical samples stored once, with its count
  → zigzag varints (small numbers, small bytes)
  → base64url

A keyboard player holds the same keys for long stretches, so their tape collapses to almost nothing. A mouse player's is bigger, but still a small POST.

The next hole: choosing the field

Replay alone isn't enough if the player picks the seed. You could record one good run, then search offline for a field where that same input survives longest, or replay a strong run on an easier field. So the server deals the field. When a run starts, the game asks for a seed, and the server returns it with a signed token: an HMAC over the seed and the time it was issued, valid for three hours.

When the run is posted, the token is checked, and the seed is marked used in Redis with SET NX, keyed to a hash of that run's tape. One dealt field goes with one run: the same tape can be posted again (under another name, after a clash), a different one can't. The daily field needs none of this, because its seed is the date, the same for everyone; the list of real asteroids it uses is checked against the one the server fetched from NASA that day.

What it rejects

  • A claimed score: replaced by the replayed one
  • A tape cut short, or run on a different seed than its token's
  • No token, a forged token, or a token reused for another run
  • A daily run with asteroids dropped from its list

Stack the Station works the same way with a simpler tape: a build is just its drop times, rounded to the millisecond, and the server drops the modules again. The cost of all this is discipline. Every rule lives in one simulation file that the browser and the server share, and a change that alters how a run plays out also changes what old tapes replay to.