Work

Case study

One Game Format, Three Runtimes, 34 Games

A definition format and runtime contract that runs the same game logic in Python, TypeScript and Rust — and a browser arcade built from it

Result:
34 games run from one registry and one format, with 196 test files enforcing the contract; adding a game means editing its config and nothing else.
Stack:
TypeScript, Python, Rust, Vite, vitest, pytest, YAML
Published:
Updated:

Context

Most game codebases grow one engine and then grow dependent on it. Porting means rewriting. The premise here was the opposite: treat the game as data and the runtime as an interchangeable contract, so the same logic runs unchanged in Python, TypeScript or Rust.

That is a systems problem more than a games problem, and it is the same shape as any integration work — the question is whether the boundary between “what the thing does” and “what executes it” is real or aspirational.

The approach

Games are data, not code paths. Each game is defined by its configuration; the runtime reads it. Adding a game means adding its definition, not touching a dispatcher, a switch statement, or a list somewhere else.

The registry is the single source of truth, and the tooling derives from it. The catalogue, the demo lists, the deployment paths and the metadata all come from one exported view of the registry rather than hand-maintained lists kept in parallel. Hand-kept lists were the original design, and they drifted — the same game appearing in three places with two of them stale.

The contract is enforced by tests, not convention. 141 TypeScript test files and 55 Python ones, including invariants that fail the build if the registry and the filesystem disagree: every game folder with a definition must be registered, no identifier may be duplicated, and every entry in the demo block must actually be a demo.

Order and count assertions were deliberately removed. Early tests pinned the registry to an exact list and an exact number, which meant every new game broke two tests that had nothing to do with it. Replacing them with structural invariants means the suite now fails only when something is genuinely inconsistent.

The part worth stealing

The interesting engineering is not the runtimes. It is the migration discipline used to replace the hand-kept lists with derived ones.

Before changing anything, the old lists were captured to a snapshot file. The new derivation was then required to reproduce that snapshot exactly, with one documented exception and a written reason for it. The parity test is permanent: if the derivation ever stops matching what the hand-kept lists produced, the suite says so.

That is the difference between “I refactored it and the tests pass” and “I can prove the behaviour did not change.” It costs one extra step and it is the step that makes a refactor reviewable by someone who was not there.

Result

34 games run from one registry and one format. The browser arcade, the catalogue and the deployment tooling are all generated from that registry rather than maintained beside it. Adding a game is a configuration change.

The transferable lesson is about derived versus maintained state. Every hand-kept list that mirrors another source is a future inconsistency with a date on it. The fix is not discipline — it is deriving the list and proving, with a snapshot, that the derivation reproduces what you had.