How we built the boxing game
A fighting game is a pile of seventy-millisecond windows. This is how it became a package: pure rules, one frame-data table, a defender who is always right, and the one thing no client may decide.
Checked against the sources on 2026-08-30
What it is, and what it deliberately is not
The boxing game is a package, @kxb/boxing, that integrates the platform’s engine (@kxb/xp) as an SDK. It is not an XP: there is no document, no level, nothing the editor can open. It imports five interfaces - an identity, a transport, a clock, an authority - and in exchange gets multiplayer against our Supabase, against two tabs on a laptop, or against a backend nobody here has seen.
That distinction was the first decision. The engine has a general document format, and a fighting game is very specific rules about very short windows. Expressing those as a level would have bent both; importing five interfaces bends nothing.
The second early decision: the game brings its own pixels. Boxing ships its art and its React scene in the package - which is what makes "lift the folder out into its own repository" a true sentence rather than an aspiration.
The build, in the order it happened
Rules first, and pure
Everything except the network layer is numbers in, numbers out - no browser, no canvas, no clock of its own. That purity is not aesthetics: the environment this was built in never fires requestAnimationFrame, so a running fight could not be watched. `bun test packages/boxing` plays whole three-round matches in milliseconds, and that is the only reason the rules can be trusted.
Watch out: If your rules need a browser to run, you cannot test a match faster than you can play one - and then you will not.
All the feel in one table
A jab is not "a fast punch". It is 70ms before it can hurt anybody, 50ms during which it can, and 130ms of commitment afterwards where you cannot defend. Those numbers are the entire feel of the game, so they live in one record per move - the frame data - and the simulation reads the table. Balancing the game means editing one file.
The numbers are in seconds, not frames, despite the genre’s vocabulary: the host supplies the clock, a test drives a match in a loop without waiting for one, and a move written in frames changes length on a 144Hz monitor. Only the sprite sheets still think in frames, and exactly one function knows both.
Give authority to whoever losing to lag would hurt most
Damage is decided by the defender, on their own client. A punch landing on me is a fact about my health, and any other arrangement loses to lag in a way players never forgive. The round clock belongs to the red corner’s client. Both are fine because being wrong about them is visible and self-correcting: a fighter a few centimetres out is snapped straight by the next packet, a bell 100ms early is a bell.
Except the result
A result is different from everything else in the game: it is written down, read back by somebody who was not there, and nothing later corrects it. So the result goes to the arbiter - the one tier no client may decide - and the report is idempotent: both clients watch the same fight end, both may report it, the first report wins and the second is handed the stored outcome rather than an error. A client that asked twice because its first ask was lost has done nothing wrong.
Watch out: A score that can be overwritten is a score somebody can overwrite. Sort your game’s facts by that sentence.
The wire, last
Five message types on three schedules, and each client predicts only its own punches - because it must, not because prediction is fun to write. The transport is one of the five imported interfaces, which is why the same match runs over Supabase realtime, over two tabs, or over a Map in a test.
The words in the code
- @kxb/xp/host
- The five ports a game imports: identity, transport, clock, authority, persistence.
- Frame data
- The one table of what every move costs and how long it takes - the whole feel of the game.
- Arbiter
- The authority tier for facts no client may decide. Here: only the result.
- memoryHost
- The in-memory host that lets a test be two players without a network.
- Wire
- What actually crosses the socket, and how to read it.
What we would tell ourselves at the start
- Tune in one file or you will never tune. Eight switch arms holding the same number is a game nobody can balance.
- Authority is not a principle, it is a per-fact decision: self-correcting facts to the client the lag would hurt; permanent facts to the arbiter.
- Make every authority call idempotent before you need it to be.
- Seconds, not frames, anywhere a host supplies the clock.
- If the dev environment cannot render the game, make the rules run without it - the constraint turned out to be the architecture.
Read the real thing
- packages/boxing in the repositoryThe module headers carry these arguments in full; the tests carry the proof.
- How we built Mau-MauThe same pattern meeting the opposite problem: a game where the client may decide nothing.
- The XP editor guideWhat building on the engine as a document looks like, for games that fit one.
This is a map, not legal or tax advice - and an honest one about how it was drawn: the Germany guide was written by a person who walked the route; most other countries were drafted with AI against the official sources and have not yet been walked by someone who did it. Laws change. Every guide carries the date it was last checked and the sources to check it yourself - and if you have been through one of these routes, your corrections are exactly what this handbook wants.


