Leaderboards you can't cheat — replaying every run
Aug 22, 2026 · The Way to School
A browser game's leaderboard is cheatable by default. The score is computed on the
player's device and the server just receives a number. Open the developer tools, send
"score: 999999", done. So we don't accept scores. We accept the run.
A whole run is one line of characters
A round of The Way to School is fully deterministic. The date is the seed; the same seed
makes the same track. What the player did is exactly one thing per row — which lane they
stood in, and whether they jumped. So an entire run can be written as one character
per row: the lane number, plus 8 if they jumped. Five lanes at most fit in three bits,
and the spare bit is the jump flag. A thousand-row run is a thousand characters.
The server takes that string and the date and replays the run from the start with
the same rules. The score that comes out goes on the board; the score the client
claimed is never looked at. Impossible runs — a lane out of range, input continuing after
death, claiming to have passed the dog without jumping, a run that never ended — are all
rejected. This check runs automatically before every deploy: sample runs at each skill level
are computed by client and server and must match to the digit, and six kinds of forgery must
all be refused.
The server decides the name too
Trusting the name the client sends leaks two things: the profanity check (just call the API
directly), and, once accounts exist, nickname ownership (anyone could submit
under someone else's account name). So the server resolves the name: a signed-in account's
nickname wins, and a guest using someone's account nickname is blocked. The profanity filter
is one file shared by client and server — two copies means one gets fixed and
the other doesn't.
One row per person per day, with a day of slack
One row per person per day, updated only when they do better. The key for "one person" is
the account ID when signed in, otherwise a browser ID. At first it was always the browser ID,
so the same account on phone and PC showed the same nickname on several rows — up to three a
day. On August 26 we switched to the account and cleaned up the old duplicates.
The date seed is the client's local date. Time zones can put it a day off
the server's UTC date, so we allow ±1 day; anything beyond that is tampering with the past or
future. Weekly is the trailing seven days; only monthly follows the calendar —
a trailing 30 days can't be called "August's number one".
Transfer codes are 8 digits
The transfer code that moves progress without an account is eight digits: the shortest
thing a child can copy down, yet a space of ninety million, so brute force runs out of requests
long before it runs out of guesses. Codes expire after seven days — they're for the moment of
moving, not for storage.
The accident this design invited
For the server to replay a run, it must hold exactly the same rules code as the
game. So the leaderboard worker bundles the game's core rules and its config. The
catch: the game deploys automatically on push; the worker does not. On August 17 we changed a
config value and deployed only the game. The server built a different track and every
submission was rejected as "send failed". Even a purely visual value in that config
file means deploying both — it now sits at the top of the project guidelines.
Never leave test records on the live board was learned the same day. Point the
end-to-end check at the real server and its test entries get written for real.
See the Hall of Fame →