Two phones, one match: the shot is the message, the receiver is the judge
VS A FRIEND on the mode screen (host a code, join a code), a ~110-line RFC6455 relay in server.py that holds no game state, and src/net.js: each phone plays itself near-side and the opponent's court is this one rotated half a turn about the net. The phone a ball flies toward makes every call on it; the striker's copy is picture. Six tests pin the rule from both seats. No reconnect yet — a dropped radio is a card and a reload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
108ed695eb
commit
f302354bb2
11 files changed
+493
-21
No files matched your search
+46
-3
@@ -20,8 +20,8 @@ anything, so if you add a `return` for an awkward deal, check the body still run
|
||||
the state-machine contracts that have historically produced the show-stoppers. The Dockerfile runs
|
||||
it as a build stage, so a red suite cannot deploy — but finding out from Coolify is the slow way.
|
||||
|
||||
Last updated 2026-08-11 (the coaching drill lands as a back-room prototype; the horde's breather restored, the boss walk-on overlay; beats.js
|
||||
extraction, this file renamed, tests gate the build). **Start with "How to work on this", "OPEN — leftovers" and "LATER —
|
||||
Last updated 2026-08-14 (the live 1v1 socket lands: VS A FRIEND on the mode screen, the relay in
|
||||
server.py, the receiver-is-judge netcode in src/net.js — phone verdict pending). **Start with "How to work on this", "OPEN — leftovers" and "LATER —
|
||||
shipping it"** (all three just above the CLOSED sections) — the first tells you how Oliver's feedback
|
||||
should be handled, the second is everything known to be outstanding in the game, the third is the
|
||||
demo/store/campaign plan, which is decided but entirely unbuilt. All four playtest rounds are closed
|
||||
@@ -411,6 +411,46 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu
|
||||
`RETURNED`, `DOUBLE KILL`) is a footnote to something you already watched happen. Sharing the loud
|
||||
FACE with the milestone cards is right; sharing their SIZE made every footnote read as a milestone.
|
||||
|
||||
## Session log: MULTIPLAYER — the live 1v1 socket (2026-08-14). PHONE VERDICT PENDING
|
||||
|
||||
Oliver: *"alright multiplayer"* — and, offered the recorded ghost-match step one, he picked the
|
||||
LIVE SOCKET instead, plus *"a new button with generated art in the main menu — the possibility to
|
||||
start or join a game"*. Built to the architecture LATER item 7 recorded; verified end to end in two
|
||||
browser panes through the real relay (pair → serve → mirrored flight → verdict → both scoreboards
|
||||
agree on the same point from opposite perspectives). 73 tests.
|
||||
|
||||
- **It is CLASSIC TENNIS between two phones** — `G.classic` with the O opponent driven by the other
|
||||
phone instead of the AI. The mode poster is **VS A FRIEND** (`mode_vs.png`, generated in the
|
||||
zombie poster's night-court style); its second step is two chips, HOST — GET A ROOM CODE / JOIN
|
||||
WITH A CODE, and either chip IS the start — a room needs a socket, not a setup screen. `#net`
|
||||
hosts and `#net-ABCD` joins from a URL. The room code lands in a modal over the frozen court
|
||||
(an empty court waiting for a stranger is the calmest court there is).
|
||||
- **The relay is ~110 lines of stdlib in server.py** (RFC6455 handshake + framing, `/ws`). It
|
||||
holds NO game state: mints a 4-letter code (no I/L/O/U/0/1 — codes are read aloud), pairs exactly
|
||||
two sockets, forwards text frames verbatim. The host serves first on both scoreboards. 75 s
|
||||
socket timeout against vanished radios; the client heartbeats every 20 s.
|
||||
- **The sync unit is THE SHOT** (`src/net.js`): each phone simulates its own court with itself as
|
||||
P; a strike sends ~9 numbers and `mirror()` (x → -x, z → COURT.L - z) rotates them half a turn.
|
||||
The 10 Hz `pos` stream drives the O sprite and carries no authority.
|
||||
- **THE ONE RULE: the phone the ball is flying toward is the only judge of that flight** — line
|
||||
call, service box, double bounce, its own swing. The striker's outbound ball is picture
|
||||
(gameplay's tennis bounce block returns early on `G.net && lastHit === 'P'`) and the verdict
|
||||
arrives as a message: `shot` (returned), `fault` (re-serve), `pt` (point, winner relative to
|
||||
sender). This is what stops two sims a latency apart double-scoring a rally — the striker's copy
|
||||
would call a double bounce before the defender's swing had happened. An `applying` latch stops a
|
||||
received verdict echoing back out. Six tests pin the rule from both seats.
|
||||
- **startNetVs/endNetVs live in beats.js** under the ownership contract, and `G.net` joined every
|
||||
other start*'s refusal list. Serving is barred while unpaired or dead (`keyPressed`), O's auto
|
||||
serve and rally AI are gated off, `restartRun` closes the socket.
|
||||
- **Honest limits, all deliberate (ponytail)**: NO reconnect-and-resume — a dropped radio is a
|
||||
CONNECTION LOST card and a reload (the recorded plan calls reconnection "the bulk of the real
|
||||
work"; a prototype must not start there). prompt() is the join UI. The scoreboard still says
|
||||
CPU. A return snaps the ball to the mirrored strike (~latency of drift eaten in one hop; the
|
||||
flight swallows it). Runs bank as classic runs. The wound-decay hook (losing IS zombification)
|
||||
is the recorded next step once the feel survives a two-phone playtest.
|
||||
- **Test on staging with two phones** — the relay deploys with the ordinary push, nothing to
|
||||
provision. Coolify's proxy passes WebSocket upgrades as-is.
|
||||
|
||||
## Session log: the far court, without spending the lens (2026-08-13)
|
||||
|
||||
Oliver: the far-side opponent is barely visible in portrait, and the lens (camZ/camF, already at its
|
||||
@@ -2880,7 +2920,10 @@ 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
|
||||
7. **MULTIPLAYER: 1v1 zombie tennis (decided worth pursuing, 2026-08-12). THE LIVE-SOCKET
|
||||
PROTOTYPE IS BUILT (2026-08-14, staging — see the session log) — Oliver chose the socket over
|
||||
the ghost-match step one. Still open from this item: the ghost match as a solo mode, the
|
||||
wound-decay hook below, reconnect-and-resume.** 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
|
||||
|
||||
Reference in new issue
Block a user