phase: 05_anonymous_lobbies_and_room_lifecycle
All green. Final report:
**Phase 05 final verification pass — complete, 100% green.**
**Verified (all 5 tasks already in `complete/`):**
- `RoomSupervisor` import, `POST /api/rooms` → 5-char Crockford code (`VBE8Y`), `GET /api/rooms/{code}` → seat list, join/session/404 flows — all live over HTTP
- `docs/ROOMS.md` present with FSM diagram, crash-isolation and "What is lost on restart" sections
- Phase tests all pass: lifecycle 4-seat `WON`→`LOBBY` (7 tests), crash isolation, GC/idle via `FakeClock` (no sleeps), FSM, tick cadence, supervisor, sessions
**Results:**
- `uv run pytest` → **1506 passed, 0 failed**; coverage **98.89%** (>90% floor; `room.py` 99%, `rooms.py` 97%, `sessions.py` 99% — all ≥95% per-module gate)
- `uv run ruff check .` → clean; `uv run pyright` → 0 errors; `uv run undercooked-levels validate` → content OK; server boots, `/healthz` 200, client at `/` 200
- e2e US-01 suite re-run after fixes: 10 passed
**Documentation — inconsistencies found and fixed:**
- `README.md`: status now lists phases 01–05 complete, next pending `06_realtime_websocket_protocol`; dropped "(phase 05, in progress)"; 3 stale "Not implemented until phase 05." references corrected
- `app/static/js/app.js` + `tests/e2e/test_us01_landing_surface.py`: placeholder string updated in lockstep to "Not implemented until phase 07." (phase 07 task 02 wires the buttons), comments updated to reflect phase-05 completion
- `AGENTS.md`: reviewed in full — §6 already documents all phase-05 modules accurately; no changes needed
**Decisions:** no code behavior changes beyond the user-visible placeholder string; no live-infrastructure touch (only a local loopback server, stopped).
**Next pending phase:** `06_realtime_websocket_protocol`.
This commit is contained in:
@@ -12,20 +12,22 @@ email, no password: you create a room, share the 5-character code, play.
|
||||
|
||||
Phases **01** (runnable service scaffold), **02** (level content, schema and
|
||||
validation), **03** (the deterministic simulation kernel: fixed-step tick,
|
||||
movement and collision, the action button, the hands, the replay goldens)
|
||||
and **04** (the kitchen pipeline: station state machines, the ticket rail,
|
||||
movement and collision, the action button, the hands, the replay goldens),
|
||||
**04** (the kitchen pipeline: station state machines, the ticket rail,
|
||||
serve scoring, freshness and the dirty loop, the round verdict with stars)
|
||||
are **complete**; the next pending phase is
|
||||
`05_anonymous_lobbies_and_room_lifecycle`. What
|
||||
and **05** (anonymous lobbies and room lifecycle: the `RoomSupervisor`,
|
||||
the guarded `Room` FSM, the in-memory session store, the lifecycle
|
||||
integration suite) are **complete**; the next pending phase is
|
||||
`06_realtime_websocket_protocol`. What
|
||||
runs today:
|
||||
|
||||
- **The service.** The Python toolchain is locked (`uv.lock`), configuration is a
|
||||
single typed, validated `Settings` object (`DEBUGPY` support included), and
|
||||
`uv run undercooked` starts a FastAPI server that serves the client shell at `/`,
|
||||
the browser assets from `app/static/`, and `GET /healthz` — which answers
|
||||
`{"status":"ok","version":"0.1.0","rooms":0}` (`rooms` stays `0` until phase 05 owns
|
||||
a supervisor) — with the security headers (CSP `default-src 'self'`) on every
|
||||
response.
|
||||
`{"status":"ok","version":"0.1.0","rooms":<live room count>}` from the room
|
||||
supervisor (phase 05) — with the security headers (CSP `default-src 'self'`) on
|
||||
every response.
|
||||
- **The content.** The recipe book and the first two campaign levels live in
|
||||
`levels/`; `sim/level.py` parses them into typed models, `sim/validation.py` runs
|
||||
PLAN §8's named rules, and `create_app` loads + validates that bundle at startup and
|
||||
@@ -174,15 +176,58 @@ runs today:
|
||||
byte-stability across two identical runs and across the settle window);
|
||||
the contract is `docs/SIMULATION.md` §"Round end — the verdict, the
|
||||
settle, the stars".
|
||||
- **The room supervisor (phase 05).** The server-side lobby shell is
|
||||
live: `server/rooms.py` mints 5-character Crockford base32 codes (collision-
|
||||
checked, never reused), enforces `max_rooms` / `max_players` capacity, keeps a
|
||||
one-token-one-room index (a token joining a second room is evicted from the
|
||||
first; a *disconnected* seat is kept for the rejoin path), and runs the 5 s GC
|
||||
(empty rooms past a 30 s grace, idle lobbies past `lobby_idle_timeout_s`, stale
|
||||
tokens). Everything runs on an injected clock (`server/clock.py`), so the tests
|
||||
drive the garbage collector with a `FakeClock` and never sleep. The lifecycle is
|
||||
testable over HTTP before the phase-06 WebSocket transport exists:
|
||||
`POST /api/rooms` (201, the session from the `X-Undercooked-Token`
|
||||
header or the body's `token` — or a server-minted token, returned
|
||||
once),
|
||||
`GET /api/rooms/{code}`, `POST /api/rooms/{code}/join` and
|
||||
`GET /api/session` — seats never leak
|
||||
other players' tokens, a malformed code 404s exactly like an unknown one
|
||||
(US-02 criterion 4), and a full room or a full process is a 503
|
||||
`{"code": "room_full"}` (US-01 criterion 7). The `Room` itself is the guarded
|
||||
FSM behind those routes — five states, a table of fourteen legal
|
||||
transitions, one 20 Hz deadline-scheduled tick task per room and crash
|
||||
isolation that closes only the room that crashed (contract: `docs/ROOMS.md`),
|
||||
and the ready-up/host flow rides on it. The in-memory **session store**
|
||||
(`server/sessions.py`) is live on top: per token it remembers the campaign
|
||||
progression — `unlocked_level` (a `WON` round raises it to `order + 1`,
|
||||
never decreases it), `best[level_id]` (a `BestRun` of `score`/`stars`/
|
||||
`completed_at`, replaced only when the score is higher), the
|
||||
`rounds_played` / `dishes_served` counters — LRU-capped at 10 000 (evicting
|
||||
the stalest, never the session being created) and wiped on restart (A6);
|
||||
`Room.set_level` asks the store's `is_unlocked`, `round_over` records the
|
||||
result into every seated player's session, and `GET /api/session` answers
|
||||
`to_public` (a fresh token reads `unlocked: 1` with an empty `best`; 400
|
||||
`no_token` without the header) (contract: `docs/ROOMS.md` "Progression").
|
||||
The **lifecycle integration suite** (phase 05 task 05) drives the whole
|
||||
lifecycle over HTTP plus direct `tick_once`: create → 3 joins → ready-up
|
||||
→ the host's start → the countdown → a scripted full-pipeline `WON` → the
|
||||
`ROUND_END` payload → `next_level` → back to `LOBBY` with the same seats
|
||||
and colours; plus a solo win with the unlock advanced, the 5th join's and
|
||||
the process-capacity 503 `room_full`, malformed ≡ unknown codes (US-02
|
||||
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 still answer
|
||||
"Not implemented until phase 07." until phases 06/07 wire the transport.
|
||||
- **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), rooms and the
|
||||
What does not exist yet: the hazards (phase 10), the
|
||||
WebSocket
|
||||
protocol (05-06 — the lobby buttons answer "Not implemented until phase 05.") and the
|
||||
protocol (06 — the lobby buttons answer "Not implemented until phase 07.") and the
|
||||
playable Canvas renderer (07). Execute phases with the `phased-execution` skill
|
||||
(`auto-phase.sh`); it always takes the lowest-numbered directory still in
|
||||
`.agents/phases/todo/`.
|
||||
@@ -195,13 +240,21 @@ readies up, and the kitchen starts. Each cook has one movement control and **one
|
||||
button** that picks up, uses and puts down whatever is in reach — one carried item at a
|
||||
time. Orders appear in the HUD with a shrinking deadline; assemble the dish and serve it
|
||||
at the window before the timer runs out. Levels escalate: more recipes, fewer stations,
|
||||
hostile floors. Nothing is saved between rounds and nothing identifies you: the browser
|
||||
hostile floors. Nothing identifies you: the browser
|
||||
keeps only a random per-tab token and the last code you typed, both in `sessionStorage`.
|
||||
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: the page loads, normalises a typed code to
|
||||
five uppercase alphanumerics (codes themselves are Crockford base32, and only get issued
|
||||
once phase 05 runs the room supervisor), and answers "Not implemented until phase 05."
|
||||
Movement keys and touch controls are defined by the phase-07/08 client and documented in
|
||||
What actually works today is the shell of that plus the server-side room shell:
|
||||
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`), and the lobby buttons still answer "Not implemented
|
||||
until phase 07." until the phase-06 WebSocket transport and the phase-07 client
|
||||
wiring replace 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.
|
||||
|
||||
## Quickstart (development)
|
||||
@@ -217,6 +270,9 @@ uv run undercooked --reload # dev reload
|
||||
uv run undercooked --levels-dir levels # point at another content dir
|
||||
curl -fsS http://127.0.0.1:8000/api/levels # the campaign cards the lobby will show
|
||||
curl -fsS http://127.0.0.1:8000/api/config # client-safe settings + protocol version
|
||||
curl -fsS -X POST http://127.0.0.1:8000/api/rooms # create a room: 5-char code + your token (201)
|
||||
curl -fsS http://127.0.0.1:8000/api/rooms/<CODE> # inspect it: the seat list, no tokens
|
||||
curl -fsS -H 'X-Undercooked-Token: <TOKEN>' http://127.0.0.1:8000/api/session # your session: unlocked, best (in memory; a restart clears it)
|
||||
```
|
||||
|
||||
`python -m undercooked` is the same command (the container uses it). A flag
|
||||
@@ -363,7 +419,29 @@ 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/server/app.py app factory, `/healthz`, `/`, `/static`, mounts the content API
|
||||
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`
|
||||
(the only server module that imports `time`; a grep
|
||||
test keeps the wall clock out of the rest of `server/`)
|
||||
src/undercooked/server/codes.py Crockford room codes: the alphabet, collision-checked
|
||||
minting (`new_code`), input normalisation (confusables,
|
||||
malformed ≡ unknown, US-02 criterion 4)
|
||||
src/undercooked/server/room.py `Room`: code, seats (the rejoin path keeps
|
||||
disconnected seats), `RoomState`, one owned tick task
|
||||
src/undercooked/server/rooms.py `RoomSupervisor`: the registry, capacity, the
|
||||
one-token-one-room index, the 5 s GC, `start`/`shutdown`
|
||||
src/undercooked/server/sessions.py `SessionRecord` + `BestRun` + `mint_token`
|
||||
+ the bounded LRU `SessionStore` (`record_result`'s win-only
|
||||
unlock bump, best-score replacement, `is_unlocked` /
|
||||
`next_level_id`, `to_public` — in memory; a process
|
||||
restart clears all progression)
|
||||
src/undercooked/server/routes_rooms.py `POST /api/rooms`, `GET /api/rooms/{code}`, `POST
|
||||
/api/rooms/{code}/join`, `GET /api/session` — the lifecycle
|
||||
over HTTP before the phase-06 WebSocket transport (session
|
||||
from the `X-Undercooked-Token` header or the body's
|
||||
`token`, minted otherwise; tokens returned once; seats never
|
||||
leak tokens)
|
||||
src/undercooked/server/routes_content.py `/api/levels`, `/api/levels/{id}`, `/api/config`,
|
||||
plus the startup gate: broken content refuses startup
|
||||
src/undercooked/server/headers.py the security-header middleware
|
||||
|
||||
Reference in New Issue
Block a user