Voxel Racers · the toolchain

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

1

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

2

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

3

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

4

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

5

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

6

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

7

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

8

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

9

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

10

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

11

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

12

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

13

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.

CheckComparedToleranceResultSince
Car physics7,200 ticks per course1e-60 differing; worst 7.1e-152026-10-21
A full race, 8 cars5,400 ticks1e-60 differing; worst 2.7e-152026-10-21
Battle race5,400 ticks1e-60 differing2026-10-21
Qualifying5,400 ticks1e-60 differing2026-10-22
Race soundsevery tick with a soundexact0 differing2026-10-21
Engine audio, 34 engines81,920 samples each1e-5worst 5.0e-82026-10-21
Arena battle3,440 and 4,399 ticks1e-60 apart2026-10-25
numpy random streams9 seeds, 90 drawsexactidentical2026-10-21
Python random streams77 draws, 48 plansexactidentical2026-10-21
Garage: cars and setups1,116 numbers1e-9identical2026-10-22

04The playbook

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: