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:
@@ -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
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user