phase: 07_canvas_client_and_hud

**Final report — phase 07 final verification pass**

- **Verified:** all 5 tasks present in `complete/`; app boots (`/healthz` 200, `/` 200, static + atlas + `/api/levels` served); `docs/CLIENT.md` module map matches the on-disk `app/static/` tree; no code defects found.
- **Tests/lint/coverage:** `bash .agents/validate.sh` → ruff clean, pyright 0 errors, `undercooked-levels validate` pass, **full pytest 1838 passed, 97.21% coverage (floor 90%)**, slow sim stage 2 passed, repo hygiene clean. `uv run pytest tests/e2e -q` → **53 passed** (all 8 suites added this phase).
- **Criteria:** (1) two tabs create→join→ready→start→serve one dish, keyboard only — green via `test_us01/02/03/04` ✔; (2) e2e green ✔; (3) canvas non-blank, pixels change after a move — `test_us04_render_smoke` ✔; (4) HUD score within 200 ms of the `served` event — asserted, green ✔; (5) zero console errors + zero off-origin requests — `test_atlas_loads` / `test_us01_landing_surface` ✔; (6) `docs/CLIENT.md` matches layout ✔; (7) 100% suite + 97.21% ✔; (8) docs pass below ✔; (9) no regression in completed phases (all prior suites green) ✔.
- **README.md fixed:** status still said "next pending phase is 07" → now 07 complete, 08 pending; the phase-07 client text buried in the phase-05 bullet → its own "canvas client, art, HUD (phase 07)" bullet; stale "What does not exist yet" (claimed movement/controls missing) → rewritten to phases 08–14; "once the room flow ships" → "live in the browser today"; Layout `app/static/` "renderer arrives with the rest of phase 07" → full shipped-client map; Layout `docs/` "CLIENT lands with phase 07" → `CLIENT.md` listed; added `gen_art_index.py` Layout entry.
- **AGENTS.md fixed:** "the eight screens" (7 listed) → "the seven screens" (+ `dom.js` is the shared builder, not a screen); e2e naming line → notes the non-story guard `test_atlas_loads.py`.
- **Decisions:** no code changes were needed; only the two doc fixes above. Docs re-verified by `test_docs_commands`/`test_docs_index` (44 passed) plus the final full gate.
- **Next pending phase:** `08_client_prediction_and_touch`.
This commit is contained in:
2026-10-10 18:03:52 -04:00
parent f26849c1f3
commit 95ca3fd1fa
78 changed files with 13650 additions and 259 deletions
+121 -40
View File
@@ -17,13 +17,15 @@ movement and collision, the action button, the hands, the replay goldens),
serve scoring, freshness and the dirty loop, the round verdict with stars)
and **05** (anonymous lobbies and room lifecycle: the `RoomSupervisor`,
the guarded `Room` FSM, the in-memory session store, the lifecycle
integration suite) and **06** (the realtime WebSocket protocol: the
integration suite), **06** (the realtime WebSocket protocol: the
`/ws/{code}` transport, the versioned JSON wire contract with inbound
guardrails, input ingestion with sequence discipline, the quantized
snapshot stream, heartbeats and seat grace — `docs/PROTOCOL.md`)
are **complete**; the next pending phase is
`07_canvas_client_and_hud`. What
runs today:
snapshot stream, heartbeats and seat grace — `docs/PROTOCOL.md`) and
**07** (the canvas client, art and HUD: the in-repo SVG atlas, the
screen state machine, the snapshot buffer with 100 ms interpolation,
the DPR-aware Canvas renderer, the DOM HUD and the desktop keyboard
input — `docs/CLIENT.md`) are **complete**; the next pending phase is
`08_client_prediction_and_touch`. What runs today:
- **The service.** The Python toolchain is locked (`uv.lock`), configuration is a
single typed, validated `Settings` object (`DEBUGPY` support included), and
@@ -221,9 +223,7 @@ runs today:
criterion 4), the seat-grace and lobby-idle expiries through the GC, and
one room's crash closing only itself (the test-only `_test_step_override`
seam on `Room`; contract: `docs/ROOMS.md` "Failure and expiry" and
"The lifecycle walkthrough"). The client buttons in the page still
answer "Not implemented until phase 07." until the phase-07 client
wiring replaces them.
"The lifecycle walkthrough").
- **The WebSocket transport (phase 06, task 02).** The room core is on
the wire: `GET /api/stats` answers the ops census (`rooms` / `players`
— the occupied seats / `sockets` — live WebSocket connections /
@@ -334,24 +334,56 @@ runs today:
merged snapshot view (`expect` / `collect` / `send_model`). The
contract, the test seams and the budget table with the measured
values are `docs/PROTOCOL.md` ("Testing the protocol").
- **The canvas client, art, and the HUD (phase 07).** The browser half
is live on top of all of this: the in-repo SVG atlas (`assets_src/`
→ the served copies in `app/static/assets/`; `gen_art_index`
regenerates the `index.json`, and a missing or duplicated symbol
fails the build, the expected ids deriving from the sim's own enums
and the `sim/stations.py` pipeline tables), the screen state machine
(the page's landing create/join, the lobby with its code copy,
level picker, ready-up, start and seat chips, the 3-2-1 countdown,
the round-end and the error screens, all on the same
`WS /ws/{code}` socket), the snapshot buffer with the 100 ms
interpolation and the event queue, the single render loop, the
DPR-aware Canvas renderer (the kitchen floor, the stations, the
y-sorted cooks, the carried items, the progress rings, the hazards,
the particles, the target highlight) and the DOM HUD (the order
rail, the score/target, the countdown with its non-colour cue, the
streak, the dirty counter, the connection indicator, the spectator
banner); the desktop keyboard input (WASD/arrows, the action
button, the emotes, the `?` help overlay — the touch surface is
phase 08) closes it out. `docs/CLIENT.md` is the module map and the
contract; the proof is the e2e suites — `test_us01_create_lobby.py`,
`test_us02_join_by_code.py`, `test_us03_ready_and_start.py` (the
lobby flow), `test_us04_move_and_cook.py` (a scripted keyboard cook
serves a soup) and `test_us04_render_smoke.py` (the canvas is
provably not blank and moves after input; the HUD score changes
within 200 ms of the `served` event), `test_us06_interp_math.py`
and `test_us06_smoothness.py` (the interpolation math, exercised
through a real page; per-tick displacement bounded with 60 ms of
injected latency, both clients converge on `ROUND_END`), and
`test_atlas_loads.py` (the atlas decodes with zero console errors;
`test_us01_landing_surface.py` asserts the page's only network
requests are same-origin).
- **The rules are tests, not promises.** The no-external-asset rule is
`tests/unit/test_no_external_assets.py`, "no level or recipe id in Python" is
`tests/unit/test_content_not_hardcoded.py`, the documentation set is guarded by
`tests/unit/test_docs_index.py`, and `bash .agents/validate.sh` is the gate the
phase pipeline runs after every task.
What does not exist yet: the hazards (phase 10) and the
in-browser
playable Canvas renderer (07 — the phase-06 WebSocket transport, including
the snapshot stream, is live but the client still answers "Not
implemented until phase 07."). Execute phases with the
`phased-execution` skill
(`auto-phase.sh`); it always takes the lowest-numbered directory still in
`.agents/phases/todo/`.
What does not exist yet: client prediction, reconciliation and the
touch surface (phase 08), the two-browser co-op E2E with
contested-resource arbitration (phase 09), the hazards and their
client FX (phase 10), the rest of the ten-level campaign (phase 11),
reconnect resilience and the 200-room stress run (phase 12), endless
mode with the session scoreboard (phase 13), and the accessibility
polish and hardening pass (phase 14). Execute phases with the
`phased-execution` skill (`auto-phase.sh`); it always takes the
lowest-numbered directory still in `.agents/phases/todo/`.
## How to play
The intended loop, once the room flow ships: open the site, **Create lobby**, read out
The loop, live in the browser today: open the site, **Create lobby**, read out
the 5-character code, everyone else **Joins** with it, the host picks a level, the room
readies up, and the kitchen starts. Each cook has one movement control and **one action
button** that picks up, uses and puts down whatever is in reach — one carried item at a
@@ -362,22 +394,41 @@ keeps only a random per-tab token and the last code you typed, both in `sessionS
The campaign does remember per token — unlocks and best scores live server-side,
in memory, and a server restart clears all progression (A6: no database).
What actually works today is the shell of that plus the server-side room
shell and the phase-06 WebSocket transport:
the page loads, normalises a typed code to five uppercase alphanumerics (codes
themselves are Crockford base32, minted by the room supervisor — `server/rooms.py`,
phase 05, which is live and testable over `POST /api/rooms` / `GET /api/rooms/{code}` /
`POST /api/rooms/{code}/join` / `GET /api/session`), a `WS /ws/{code}`
client can already create a room over HTTP, handshake (`hello` → `welcome`
+ `lobby`), ready up and keep alive — the full contract, including the
guardrails and the close codes, is `docs/PROTOCOL.md` — but the lobby
buttons in the page still answer "Not implemented
until phase 07." until the phase-07 client
wiring replaces them. The ready-up, the 20 Hz tick ownership, the session store
(the unlocks and best scores, in memory — a server restart clears all
progression) and the lifecycle integration suite are live. Movement keys and
touch controls are defined by the phase-07/08 client and documented in
`docs/CLIENT.md` once that phase writes it.
The page does that loop end to end: it loads,
normalises a typed code to five uppercase alphanumerics (codes
themselves are Crockford base32, minted by the room supervisor —
`server/rooms.py`, phase 05, also testable over `POST /api/rooms` /
`GET /api/rooms/{code}` / `POST /api/rooms/{code}/join` /
`GET /api/session`), the landing's **Create lobby** / **Join with code**
open a `WS /ws/{code}` socket (`hello` → `welcome` + `lobby`), the lobby
shows the code, the level picker (fed by `/api/levels`), the ready and
start controls and the seat chips, the 3-2-1 countdown runs, and the
round-end screen reports the verdict — the full contract, including the
guardrails and the close codes, is `docs/PROTOCOL.md`, the client's own
contract is `docs/CLIENT.md`. The playable Canvas renderer, the HUD and
the desktop keyboard controls are live (below; the in-game `?` help
overlay renders the same rows) — the touch controls arrive with phase 08.
The ready-up, the
20 Hz tick ownership, the
session store (the unlocks and best scores, in memory — a server restart
clears all progression) and the lifecycle integration suite are live.
### Controls
- **W / A / S / D** — move (or the arrow keys)
- **Space / Enter** — action (pick, work, set down, serve)
- **1 / 2 / 3 / 4** — emote (wave, cheer, thumbs up, thumbs down)
- **?** — help overlay
- **Esc** — close help / leave the room
The goal is the HUD's target score before the level's deadline: an order
ticket names a dish, and the action button does everything a hand does —
pick the ingredient off a shelf, work it on a station (hold the button
while the station works), plate it and serve it at the window. One
carried item at a time; a set-down marks the item so a partner can
pass it. `?` opens the help overlay (the same rows, rendered in-game);
`Esc` closes it — or leaves the room. The `?controls=off` URL param
disables the bindings (the touch surface's seam, phase 08).
## Quickstart (development)
@@ -419,6 +470,7 @@ uv run undercooked-levels show 01_first_kitchen --grid # resolved level + reach
uv run undercooked-levels validate # the content gate; --json for scripts
uv run undercooked-levels new 03_riverboat # scaffold a level from the recipe book
uv run undercooked-levels axes # escalation-axis table (phase 11's curve)
uv run python -m undercooked.tools.gen_art_index # rebuild app/static/assets/index.json from assets_src/
```
`undercooked-levels` is a thin shell over the loader and the PLAN §8 rules
@@ -430,7 +482,13 @@ and never from the current directory, so it works from any path. Exit codes:
failure, unknown id, refused overwrite, usage error). The schema, every field,
the overlay trick and a "which rule did I just hit" table live in
[docs/LEVELS.md](docs/LEVELS.md); `tests/unit/test_content_not_hardcoded.py`
is what keeps levels and recipes out of the Python.
is what keeps levels and recipes out of the Python. The in-repo SVG art
(phase 07 task 01) is guarded the same way: `gen_art_index` fails with the
missing id named when a symbol is absent or duplicated, and its expected set is
derived from the simulation's own enums and pipeline tables, so a new
`ItemKind`/`ItemForm` the sim can produce breaks the build until a symbol is
drawn for it (guards: `tests/unit/test_assets.py`,
`tests/e2e/test_atlas_loads.py`).
Run the checks:
@@ -543,6 +601,11 @@ src/undercooked/sim/replay.py the replay harness: the input-script format, the
src/undercooked/tools/levels.py the `undercooked-levels` author CLI — validate, list,
show [--grid], new, axes; same loader, same rules, no
hard-coded content list (docs/LEVELS.md "Authoring workflow")
src/undercooked/tools/gen_art_index.py the SVG atlas index generator — rebuilds
app/static/assets/index.json from assets_src/; the expected
ids derive from the sim's enums + the station pipeline
tables, a missing or duplicated id fails (guards:
tests/unit/test_assets.py)
src/undercooked/server/app.py app factory, `/healthz`, `/`, `/static`, mounts the
content API and the room API; installs the supervisor
src/undercooked/server/clock.py injectable time: `Clock`, `SystemClock`, `FakeClock`
@@ -572,8 +635,25 @@ src/undercooked/server/headers.py the security-header middleware
.env.example every configuration variable, with its default
levels/ data-driven content (YAML): the recipe book (A8) and the
first two campaign levels (01_first_kitchen, 02_double_shift)
app/static/ browser client: the ES-module shell today; Canvas 2D renderer
and this repo's SVG art arrive with phase 07
assets_src/ the hand-authored SVG art (tiles, stations, items, cooks,
fx, ui) — source for the served copies in
app/static/assets/ (a guard test keeps them in sync)
app/static/ the browser client (ES modules, no bundler):
the composition root (`js/main.js`), the screen
state machine (`js/app.js`) and the seven screens
(`js/screens/`, the no-innerHTML DOM builder
`dom.js`), the snapshot buffer + 100 ms
interpolation (`js/net/`), the single render loop
(`js/render/loop.js`), the DPR-aware Canvas 2D
renderer (`js/render/canvas.js` + `camera.js` +
`fx.js`), the DOM HUD (`js/ui/hud.js`), the
desktop keyboard input (`js/input/keyboard.js`)
and the help overlay (`js/ui/help.js`); the
in-repo SVG atlas with its `index.json`, the
palette (`assets/palette.css` +
`js/render/palette.js`, kept hex-identical by a
test) and the atlas loader (`js/render/atlas.js`)
(contract: docs/CLIENT.md)
tests/ unit/, integration/, e2e/ (one Playwright suite per story) and
sim/ (the pure kernel: grid, entities, purity guard, validation,
the seeded RNG and the tick; plus the replay harness — committed
@@ -587,10 +667,11 @@ docs/ DESIGN.md, LEVELS.md (the level & recipe YAML contract),
SIMULATION.md (the tick contract), ROOMS.md (rooms &
lobby lifecycle), PROTOCOL.md (the v1 wire contract —
message tables, error/close codes, limits, the full-round
sequence, the test seams and the budget table), TESTING.md
(how this project
is verified), DEPLOYMENT.md; CLIENT lands with
phase 07 (the index is PLAN.md §13)
sequence, the test seams and the budget table), CLIENT.md
(the client module map, the render loop, the snapshot
buffer, the input, the assets), TESTING.md (how this
project is verified), DEPLOYMENT.md (the index is
PLAN.md §13)
.agents/ PLAN.md, user_stories/, phases/, validate.sh — the phased-execution plan
```