How to

How we built a Snake game with verified challenge links and no server

Our Snake game has daily challenges, 20 mazes, friend challenges and ghost races, and no backend. Here is how a seed, a turn log and a pure rules engine make that work.

The rules engine knows nothing about the screen

Everything that decides a run lives in one pure TypeScript module: movement, collisions, walls, the speed ramp, special food, combos and the two-turn input buffer. It has no access to the page, the clock or the canvas. The renderer asks it for the state of the board and draws it; the controller calls tick() on a fixed interval.

That separation is what makes everything else possible. A module that cannot see the outside world behaves the same way every time it is given the same inputs, so it can be tested in thousands of runs without a browser, and a run can be played again exactly.

Time is counted in ticks, not milliseconds

Real time is noisy: a busy phone drops frames and a background tab is throttled. So the engine keeps its own game time, the sum of the tick lengths it has played. Special food lasts eight seconds of game time, and a slow-down lasts six. Two machines playing the same turns therefore reach the same state on the same tick, whatever their frame rate.

A seeded random generator makes food predictable to the game

Food positions come from a small seeded generator (mulberry32) rather than Math.random(). Give it the same seed and it produces the same sequence of numbers, so the same food appears in the same places in the same order. The Daily Challenge uses this directly: the seed and the maze layout come from a hash of the UTC date, so everyone plays the same board that day.

A whole run fits in a short code

Because the engine is deterministic, a run is completely described by its starting conditions and the turns the player made. The replay code stores:

  • the format version, mode, board size, seed and the maze level or daily date;
  • the claimed score, final length and number of ticks;
  • every accepted turn, as the ticks since the previous turn times four plus the direction.

Each number is packed as a variable-length integer, and the bytes are written in URL-safe base64. A typical Classic run becomes a code of a few hundred characters that fits in a link: /play/snake#c=…. The code sits after the #, so it is never sent to any server.

Verifying a friend’s score on your own device

When you open a challenge link, your browser decodes it and plays the run again in a fresh engine, without drawing it. The run must be legal turn by turn, end on exactly its last tick, and reproduce the claimed score and length. If any check fails, the page says the link couldn’t be verified and shows no score. Every field has a fixed range and bad input returns nothing instead of throwing an error, so a tampered or truncated link can’t break the page.

This proves the run is possible on that seed, but not who played it. For friendly challenges that is the right trade-off for a site with no accounts.

Racing a ghost

The same replay drives the ghost race. A second engine is fed the stored turns and advanced on your game clock, so the ghost moves exactly as the original player did, tick for tick, beside you. It is drawn see-through with a dashed line, it can’t collide with you, and its food isn’t shown. Your personal top ten runs on each device are stored the same way, so you can race your own best.

Built to be tested

The pure engine made the test suite cheap to write. Unit tests cover the rules, special food, combos, replays and achievements. Every one of the 20 mazes is checked for reachability with a flood fill, a clear start and no food inside walls. Tampered claims and logs are caught, and a malformed code is rejected without throwing. Another test replays a seed and a turn log and checks the result matches the original run exactly.

Try it

Play Snake, finish a Classic run and send the challenge link to a friend. The skins are real species patterns from the encyclopedia, such as the Indian cobra, and every run ends with a sourced fact.

Questions people ask

Can a challenge link prove someone really played the run?
No. Re-simulating the run proves it is possible on that seed under the game's rules, but not that a person played it rather than a program. That is the limit without a server, and it is fine for friendly challenges.
Why not use a leaderboard?
A global leaderboard needs a server to store scores and check them. The site is static on purpose, so scores stay on your device and challenges travel in links.
How long is a challenge link?
About 210 characters of code for a 45-food Classic run and about 430 for an 80-food run, because each turn is stored as a small number.
More guides

Try “krait”, “cobra” or Naja. Typos are fine.