From a Python game to a browser game, in thirteen steps
One rule sits under everything here: the Python game is the specification, and it never changes for the web. Every tool below does one of two jobs — it asks the game to compute something and writes it down, or it proves the browser does exactly what the game does. Here is the whole assembly line, with what goes in and what comes out at each station.
- The game does the heavy lifting: anything it computes once is baked into files.
- JavaScript gets only the rules that run every tick, and borrows every number from the Python.
- Recordings of the real game are the referee — ten checks, each seen failing on a planted bug before it was trusted.
- The rest is plumbing: per-course builds, packaging under the host's limits, and a one-command release on the fleet.
01What crosses, and what does not
Baked by the game
Course building (about 2,900 lines of course generation), meshing, textures, car models, the race's starting grid, voice lines, music and every sound effect. In the browser these are just files, which is the whole trick.
Rewritten in JavaScript
About 9,000 lines: race rules (~4,500), battles, hazards and shows (~2,100), the interface (~2,200) and the car's physics (~280, ported first). These run every tick, so they must run in the browser.
Proven equal
Car, race, battle, qualifying, sounds, engine audio, arena, both random-number generators and the garage — ten checks, each witnessed failing on a planted bug before it was trusted.
02The line, step by step
Cut a boundary through the engine
First, decide where racing ends and the engine begins. Racing used to reach into the engine in 149 places across 19 modules. It now reaches it only through named extension points: 76 uses in 2 modules, at about 0.2 µs a call. The list of points is also the list of things the browser runtime has to provide.
engine + racing → named points, counted by a check that fails if they grow
Bake the course
Let the game build the track, then save exactly what it built. The course is built in a throwaway world by the game's own builder, mesher, textures and car model, then written out in the GPU's own vertex format — four corners per face, six 16-bit numbers each. Only a band either side of the road is baked, and faces buried far below it are dropped. The bake refuses to ship if under 99% of the road is drawn.
course → meta.json, mesh.bin, tex.bin
Rules as data
Pull the numbers out of the Python, so JavaScript never keeps its own copies. Every upper-case constant in the racing modules is read straight out of the Python — 865 of them per course today, 310 when this started — together with the car's specification, the block palette, collision boxes and the venue's line, forks, grip and corners. The collision grid is written per cell, plus the hollows under each column so space courses are not solid rock.
racing modules → rules.json, cells.bin, states.bin
Record the real game
Film the original, tick by tick, so there is something to check against. A real race runs in a throwaway session and every tick is written down: the keys, your car before and after, every rival, and every event the game raised — sounds, banners, the announcer's queue. Battle and qualifying are recorded the same way, and so are reference draws from both random-number generators.
racing.tick → race.json, racetrace.json, rng.json
Render every sound
Every vroom, crash and announcer call comes from the game itself. Every sound a race can make is rendered through the game's own recipes, plus the drivers' and announcer's recordings, and encoded with its mixing metadata. About 720 sounds per course today — 412 on the first course. Engines are rendered separately for their own parity check.
mixer recipes → sounds, engine traces
The garage and the arena
The cars, the drivers and the arena get the same treatment. Eleven cars, twelve drivers and six setups go into one file with 36 checks against the game's own classes; six arena themes are baked like courses.
garage → garage.json; arena → six packs
The JavaScript runtime
Now the only part that is actually rewritten. Each file is named for the Python it mirrors — car, race, battle, shows, audio, camera, terrain — and holds no numbers of its own. Both random generators are reimplemented bit for bit. The race and car code has no web page in it, so the same code runs in the browser and in the checks.
rules.json + recordings → a game loop in WebGL2
Prove it, stepwise
The referee: replay, compare, repeat. Each tick, the JavaScript is put into the game's exact recorded state, steps once, and every field is compared at a tolerance of a millionth (a hundred-thousandth for audio samples). A separate free run just races both and reports when chaos parts them — information, not a gate.
recordings + runtime → pass / fail, per field, per tick
Build a course end to end
One course, start to finish, through every station: bake, sounds, car recording, race, battle and qualifying recordings, then eight checks. A course is rolled out only when every check passes and nothing it uses is missing from the web runtime.
course → build record + a site pack, or a refusal that names why
Package under the host's limits
Fit everything through the host's door. Every file is compressed and cut into parts of at most 24 MiB, because the host refuses anything over 25. A course's sounds become one indexed file. File names carry a content hash and a long cache header, so a returning player downloads nothing they already have.
packs → site files, versioned
Release on the fleet
Do it for every course at once, without trusting luck. One command builds every course across three machines at once, fetches the results into an empty folder, reads every pack back, rebuilds locally anything the fleet dropped, and never deploys a stale pack.
all courses → a verified release
Publish together
Ship the notes and the game as one. The write-up and the game deploy as one site, because a deploy replaces the whole site and publishing one would delete the other.
release + write-up → the live site
Watch for drift
Keep the two games from quietly wandering apart. After every ship, a tool lists the racing modules changed since the two games were last in sync. New numbers travel with the next release; new rules need a hand port, and it says which.
git history → "numbers" or "RULES" per changed module
03The proof, check by check
Every check here was first watched failing on a bug I planted on purpose. A smoke alarm you have never heard go off is just a ceiling decoration.
| Check | Compared | Tolerance | Result | Since |
|---|---|---|---|---|
| Car physics | 7,200 ticks per course | 1e-6 | 0 differing; worst 7.1e-15 | 2026-10-21 |
| A full race, 8 cars | 5,400 ticks | 1e-6 | 0 differing; worst 2.7e-15 | 2026-10-21 |
| Battle race | 5,400 ticks | 1e-6 | 0 differing | 2026-10-21 |
| Qualifying | 5,400 ticks | 1e-6 | 0 differing | 2026-10-22 |
| Race sounds | every tick with a sound | exact | 0 differing | 2026-10-21 |
| Engine audio, 34 engines | 81,920 samples each | 1e-5 | worst 5.0e-8 | 2026-10-21 |
| Arena battle | 3,440 and 4,399 ticks | 1e-6 | 0 apart | 2026-10-25 |
| numpy random streams | 9 seeds, 90 draws | exact | identical | 2026-10-21 |
| Python random streams | 77 draws, 48 plans | exact | identical | 2026-10-21 |
| Garage: cars and setups | 1,116 numbers | 1e-9 | identical | 2026-10-22 |
04The playbook
- Bake, don't port. If the game computes it once, the game writes it down.
the less you rewrite, the less you have to prove.
- Port the random streams exactly. A different random number is a different race.
randomness is part of the behaviour; treat it like any other rule.
- Prove parity by recording, stepwise, every field.
"looks the same" is an opinion; a recording is evidence.
- Witness every check failing before trusting it to pass.
a test that has never failed may simply be unable to.
- Bundle by following imports, and refuse two modules claiming the same name.
let the code say what it needs instead of keeping a list by hand.
- Do heavy work on the fleet, never on the machine I am working at.
keep the machine you think on free to think.
- Ship per-course packs, so a player downloads the course they chose.
make people pay only for what they asked for.
- Give touch screens an analog thumbstick, not keyboard keys painted on glass.
design the controls for the device in the player's hand, not the one on your desk.
- Port a rule change soon after it lands, one gap at a time.
small drifts are cheap to close; big ones become projects.
05Tools for making more
Courses, power-ups and events are also built from checklists, so a new one is a matter of filling in a spec rather than remembering fifteen tables in eight files — which, honestly, I could not do reliably:
- A course kit checks that a course has every piece it needs, scaffolds a new one from a sibling, plans its layout and its music.
- A racers kit lists, checks, creates and reviews power-ups, courses and events.
- A road linter finds solid blocks standing in the drivable band; others check headroom, dark stretches of road and where sounds can be heard.
- Tuning tools fit drift, balance the cars against each other and simulate battles.