LATER item 7: 1v1 multiplayer — shot-relay architecture, ghost match first

Decided with Oliver 2026-08-12. Records why tennis is the friendly case (the
sync unit is the shot, alternation already enforced by ball.lastHit, each
client authoritative for its own contacts), what the codebase already brings
(deterministic sim, fixed dt, beats.js ownership contract), the zero-dep
WebSocket path (~120 lines of stdlib RFC6455 in server.py, friend-code
rooms), the honest cost (reconnect-and-resume is the real work), and that
step one is the GHOST MATCH over plain HTTP — half the netcode falls out of
it as a byproduct. Plus the hook: score-is-the-body on both players, losing
is zombification.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-12 00:13:53 +02:00
1 parent 78292a2d53
commit 9b9f18d312
1 file changed
+29
+29
View File
@@ -2349,6 +2349,35 @@ never an interstitial. An interstitial after death would land exactly in the "on
is not), and canvas perf at `RES` 4 — the third place that risk lands, after OPEN 1 and item 2
above. `matchMedia('(pointer: coarse)')` does still work there, so the mobile branches are fine.
7. **MULTIPLAYER: 1v1 zombie tennis (decided worth pursuing, 2026-08-12).** Tennis is one of the
friendliest genres in existence for netcode, and this codebase is unusually ready for it — record
the decision and the architecture so neither is re-derived:
- **The sync unit is THE SHOT, not world state.** One ball, one legal hitter at a time
(`ball.lastHit` already enforces alternation), ~one interaction per second, and each flight's
~1 s swallows 100-150 ms of latency before the opponent needs the data. Each client is
authoritative for its OWN contacts (your swing window is judged on your phone — lag never eats
your tap); a dumb relay forwards shot events (~9 numbers: launch pos/vel/gravity/quality) and
point results. No rollback, no shared physics contention.
- **What already exists toward it**: the sim is deterministic-driven (the suite plays the real
modules headless on seeded randomness), `update()` steps fixed dt, ball state is one small
object, `beats.js` is where `startNetMatch()` would live under the court-ownership contract,
and the whole serve/swing/volley system tuned for the zombie match IS the netplay input model.
- **Zero-dep server**: python stdlib has no WebSocket, but RFC6455 handshake + framing is ~120
lines of stdlib and `server.py` is already the one process holding state. Rooms = a 4-letter
friend code in a dict. No accounts, no matchmaking service.
- **The honest cost is the last 20%**: phone radios (screens lock, apps background, networks
switch — reconnect-and-resume is the bulk of the real work), cross-continent mush, and
hitter-authoritative trust (a plausibility check server-side is enough for friend codes; do
not build ranked-stranger anti-cheat). Prototype = a few focused sessions; robust = weeks of
paper cuts.
- **STEP ONE IS THE GHOST MATCH, not the socket.** Record a run's shot timings + placements
(tiny data), serve it over plain HTTP from the existing server, play against the recording —
racing-game ghost logic. Zero realtime infrastructure, feels close to a duel, and half the
netcode (shot serialization, replay determinism) falls out of it as a byproduct.
- **The hook that makes it ours**: score-is-the-body applied to BOTH players — every point you
lose, YOUR sprite decays a wound stage, so the loser of a long match is a shambling one-armed
wreck by match point. Losing IS zombification. Nobody else's tennis game does that.
**Expectations, so nobody is surprised:** with no marketing, 20-100 units in year one is the honest
central estimate (~$40-210 net, i.e. under Apple's fee at the low end). The monetization model is
worth maybe $200 either way; the video channel is worth ~100x that. Ship paid because it is cheap and