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
+137
View File
@@ -0,0 +1,137 @@
// Undercooked — the wire enums (phase 07 task 03).
//
// The client's vocabulary for the sim's StrEnums (src/undercooked/sim/model.py)
// and the protocol's `ErrorCode` (src/undercooked/server/protocol.py). The
// wire carries the *string values* (a StrEnum's `.value`); these frozen sets
// are what `net/decode.js` maps against and what a frame's values are
// checked against.
//
// Drift is a build failure, not a surprise: `tests/unit/test_client_enum_parity.py`
// reads this file and asserts each set is set-equal to the Python enum, in
// both directions (the same guard pattern as `test_protocol_doc_sync.py`).
// Add a value to the sim and forget it here (or vice versa) and the gate
// fails, naming the missing value.
/** The terrain under a grid cell (`TileKind`). */
export const TILE_KIND = Object.freeze([
"floor",
"counter",
"wall",
"void",
]);
/** The kitchen fixture types (`StationKind`). */
export const STATION_KIND = Object.freeze([
"spawn_shelf",
"chop_board",
"stove",
"prep",
"serve",
"sink",
"trash",
]);
/** Ingredient / dish types (`ItemKind`). */
export const ITEM_KIND = Object.freeze([
"tomato",
"onion",
"lettuce",
"dough",
"patty",
"rice",
"fish",
"plate",
]);
/** The preparation stage of an item (`ItemForm`). */
export const ITEM_FORM = Object.freeze([
"raw",
"chopped",
"cooking",
"cooked",
"burnt",
"plated",
"dirty",
]);
/** The four directions; the wire's `f` is an index into this list (PLAN §4: 0-3). */
export const FACING = Object.freeze(["up", "right", "down", "left"]);
/** The transient FX/log event kinds (`EventKind`; `encode_events`' `k`). */
export const EVENT_KIND = Object.freeze([
"chopped",
"cooked",
"ready",
"served",
"burnt",
"order_missed",
"fire_started",
"denied",
"score",
"stunned",
"washed",
"trashed",
"round_end",
"wasted",
"picked",
"dropped",
"handed_off",
"trashed_already_empty",
"tick_slow",
"err",
"resync",
"ghost_on",
"ghost_off",
"ghost_park",
]);
/** The stable rejection codes (`ErrorCode`, docs/PROTOCOL.md "Error codes"). */
export const ERROR_CODE = Object.freeze([
"unknown_type",
"bad_json",
"bad_field",
"too_big",
"rate_limited",
"not_seated",
"not_host",
"level_locked",
"wrong_state",
"room_full",
"room_gone",
"protocol_mismatch",
"evicted",
"internal",
]);
// The extras the renderer (task 04) needs. Not in the parity gate's
// seven, but the same discipline applies: the wire's short strings.
/** What a cook is doing this tick (`PlayerState`; the wire's `s`). */
export const PLAYER_STATE = Object.freeze([
"idle",
"moving",
"using",
"stunned",
"disconnected",
]);
/** A station's lifecycle (`StationState`; the wire's `s`). */
export const STATION_STATE = Object.freeze([
"idle",
"working",
"ready",
"busy",
"dirty",
"blocked",
]);
/** The hazard families (`HazardKind`; the wire's `t`). */
export const HAZARD_KIND = Object.freeze([
"fire",
"spill",
"moving_obstacle",
"rush",
]);
/** The terminal outcome of a round (`RoundResult`). */
export const ROUND_RESULT = Object.freeze(["won", "lost", "aborted"]);