From a Python game to a browser game, in thirteen steps
The rule underneath everything: the Python game is the specification and never changes for the web. Each tool below either asks the game to compute something and writes it down, or proves that the browser does exactly what the game does. Here is the whole line, with what goes in and what comes out.
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.
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
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
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
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
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 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
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
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
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
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
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
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
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
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 deliberately planted bug.
| 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.
- Port the random streams exactly. A different random number is a different race.
- Prove parity by recording, stepwise, every field.
- Witness every check failing before trusting it to pass.
- Bundle by following imports, and refuse two modules claiming the same name.
- Do heavy work on the fleet, never on the machine I am working at.
- Ship per-course packs, so a player downloads the course they chose.
- Give touch screens an analog thumbstick, not keyboard keys painted on glass.
- Port a rule change soon after it lands, one gap at a time.
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:
- 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.