Scaffold project and author 14-phase roadmap
Browser co-op kitchen game: FastAPI + native WebSockets, server-authoritative 20 Hz simulation, in-memory anonymous lobbies, data-driven YAML levels, Canvas 2D vanilla ES-module client with locally authored SVG art and no external CDNs. .agents/PLAN.md records the LOCKED decisions; .agents/user_stories/ holds the 12 flow contracts; .agents/phases/todo/ breaks them into 14 sequential, test-gated phases (71 task files), each leaving the app launchable and the suite green. LLM-Generated: true
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
# Phase 11 — The ten-level campaign and progression
|
||||
|
||||
**Story:** `US-07_campaign_progression`
|
||||
**Context:** `.agents/PLAN.md` §6 (levels), §12 (escalation axes), `docs/LEVELS.md`
|
||||
|
||||
## Objective
|
||||
Ship the campaign: author levels 3–10 on the escalation axes, make progression (unlock,
|
||||
best score, next-level flow) real across a session, and prove the curve climbs — every
|
||||
level measurably harder than the last by the metrics, not by opinion.
|
||||
|
||||
## Dependencies
|
||||
- `10_hazards_and_escalation_mechanics` (complete) — hazards, pressure model, solver/fairness hooks
|
||||
- `05_anonymous_lobbies_and_room_lifecycle` (complete) — session progression store
|
||||
- `07_canvas_client_and_hud` (complete) — level picker, result screen `Next level`
|
||||
|
||||
## Tasks
|
||||
1. `01_author_levels_03_to_06.md` — two-room split, first conveyor, tight galley, spill kitchen
|
||||
2. `02_author_levels_07_to_10.md` — firehouse, rush gauntlet, dark kitchen, the finale
|
||||
3. `03_escalation_curve_gate.md` — per-level metrics computed and asserted monotonic non-decreasing
|
||||
4. `04_campaign_flow_and_level_picker.md` — unlock gating, next/retry flow, best scores, host picker UX
|
||||
5. `05_campaign_playthrough_suite.md` — scripted 10-level playthrough proving unlock order and persistence
|
||||
|
||||
## Testing & Quality
|
||||
- Content: `uv run undercooked-levels validate` clean; manifest count test never hard-codes the
|
||||
number of levels; every level's `id`, `order_book`, `hazards`, and `stars` documented in
|
||||
`docs/LEVELS.md` with a doc-coverage test.
|
||||
- Curve: `tests/sim/test_escalation_curve.py` — for consecutive levels, the composite difficulty
|
||||
score (defined in the task) must be non-decreasing, with at least 7 of 9 strict increases
|
||||
across the campaign; solver completion time must rise or the star target must rise.
|
||||
- E2E: full campaign playthrough (fast-forced via the test clock), unlock gating enforced
|
||||
server-side (a client cannot start a locked level), best score round-trip.
|
||||
- Coverage: **>90%** on new/modified code.
|
||||
- The phase is "Complete" only when the **entire** project test suite passes 100%.
|
||||
|
||||
## Documentation & Consistency
|
||||
- The final pass reviews `README.md` and `AGENTS.md` in full against the project as it
|
||||
stands after this phase — fixing every inconsistency found, **including pre-existing
|
||||
ones not caused by this phase**.
|
||||
- Tasks complete `docs/LEVELS.md` (authoring guide + per-level sheet), `README.md` campaign
|
||||
list, `docs/DESIGN.md` difficulty model.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] 10 shipped levels validate clean, and `tests/` still contains no hard-coded level list
|
||||
- [ ] `uv run pytest tests/sim/test_escalation_curve.py -q` → green (non-decreasing composite, ≥ 7 strict increases)
|
||||
- [ ] `uv run pytest tests/e2e/test_us07_campaign_progression.py -q` → green (locked level refused server-side)
|
||||
- [ ] a scripted session completes levels 1→10 in order with best scores persisted and displayed
|
||||
- [ ] `docs/LEVELS.md` documents every level and every field (doc-coverage test green)
|
||||
- [ ] full test suite **100% passing**, coverage >90%
|
||||
- [ ] README.md and AGENTS.md are accurate and consistent with the whole project (verified in the final pass)
|
||||
- [ ] no behavior change in completed phases
|
||||
@@ -0,0 +1,47 @@
|
||||
# Task 01 — Author levels 3–6
|
||||
|
||||
**Phase:** `11_campaign_levels_and_progression` · **Story:** `US-07_campaign_progression`
|
||||
|
||||
## Objective
|
||||
Build the middle of the curve: four levels that each introduce exactly one new pressure and
|
||||
teach it before testing it.
|
||||
|
||||
## Work
|
||||
1. Author `levels/03_two_rooms.yaml` … `levels/06_spill_kitchen.yaml` to the schema, each with
|
||||
`id`, `name`, `blurb`, `difficulty`, `time_limit`, `target_orders`, `order_book`,
|
||||
`stations`, `hazards`, `stars`, `spawn_points`, `grid`:
|
||||
- **03 Two Rooms** — kitchen split by a wall with two doorways; one side holds the chop +
|
||||
pot, the other the stove + serve window; teaches route cost. Hazards: none. Difficulty 3.
|
||||
- **04 Conveyor** — first `mode: conveyor` counter row (deterministic 1-tile-per-
|
||||
`conveyor_period` shift, wraps at the end into a `drop_slot` that returns items to the
|
||||
spawn shelf): teaches timing your grab. Difficulty 4.
|
||||
- **05 Galley** — 14 × 6 corridor map, 2 spawn points, tight reach overlaps, no hazards but
|
||||
`max_orders: 5` and a 2-recipe book; teaches contested space (the phase-09 arbitration
|
||||
shows up here naturally). Difficulty 5.
|
||||
- **06 Spill Kitchen** — `hazards.spills.enabled` with 3 `seed_cells`, `every_n_th_entry: 1`,
|
||||
one sink shared by both halves; mop-or-lose tension. Difficulty 6.
|
||||
2. Each level gets a fixture test in `tests/sim/test_level_playability.py` (parametrized by the
|
||||
manifest, never by a hard-coded list): a greedy scripted bot reaches `target_orders` within
|
||||
`time_limit` for 1–4 player counts, the star thresholds are strictly ordered
|
||||
(`1 < 2 < 3` and 3-star requires beating the target by the documented margin), and the
|
||||
reachable-set assertions from phase 02 hold.
|
||||
3. `docs/LEVELS.md` — one sheet per level: intent, the one new pressure, layout ASCII map, the
|
||||
metric snapshot from task 03 (filled by task 03, placeholder until then), and authoring notes.
|
||||
4. Balance pass: create `tools/curve.py` now (stdlib + the sim package only) — it loads the
|
||||
manifest, runs the greedy bot per player count, and prints the per-level metric table
|
||||
defined in task 03; task 03 adds the monotonicity gate on top. Adjust `time_limit`,
|
||||
`target_orders`, and `order_book` weights until the composite score is non-decreasing for
|
||||
1→6, and record the numbers in the phase report.
|
||||
|
||||
## Testing & Quality
|
||||
- Sim playability fixtures per level + validator; no hard-coded level lists anywhere.
|
||||
- Coverage: **>90%** on any changed sim/validator code.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run undercooked-levels validate` → clean with 6 levels
|
||||
- [ ] `uv run pytest tests/sim/test_level_playability.py -q` → green for 1, 2, and 4 players on every shipped level
|
||||
- [ ] each new level introduces exactly one new pressure (stated in `docs/LEVELS.md` and checkable from the YAML's diff vs the previous level)
|
||||
- [ ] composite difficulty is non-decreasing from level 1 through 6 (numbers in the phase report)
|
||||
- [ ] full test suite **100% passing**
|
||||
- [ ] README.md/AGENTS.md updated for anything user-visible this task changes (the phase's final pass re-checks both project-wide)
|
||||
- [ ] no behavior change in completed work
|
||||
@@ -0,0 +1,42 @@
|
||||
# Task 02 — Author levels 7–10
|
||||
|
||||
**Phase:** `11_campaign_levels_and_progression` · **Story:** `US-07_campaign_progression`
|
||||
|
||||
## Objective
|
||||
The back half: layered hazards and pressure, ending on a level that uses everything the
|
||||
campaign taught.
|
||||
|
||||
## Work
|
||||
1. Author `levels/07_firehouse.yaml` … `levels/10_finale.yaml`:
|
||||
- **07 Firehouse** — high-burn recipes (`burn_ticks` tightened), 2 stoves, sink central,
|
||||
`hazards.fire.enabled` with `max_fires: 3`; teaches extinguish-vs-cook triage. Difficulty 7.
|
||||
- **08 Rush Gauntlet** — `hazards.rush.waves` (3 waves at 40 s / 95 s / 140 s,
|
||||
`order_time_mult` 0.8 / 0.65 / 0.5, labels `LUNCH`, `RUSH`, `LAST CALL`), no physical
|
||||
hazards; pure time pressure. Difficulty 7.
|
||||
- **09 Obstacle Course** — 2 conveyor rows + 1 `pingpong` obstacle crossing the only bridge;
|
||||
`hazards.spills` disabled to keep one variable legible. Difficulty 8.
|
||||
- **10 Finale** — two rooms + conveyor + fire + a 2-wave rush + `dark_kitchen` modifier for
|
||||
the final 30 s; 3 recipes; `time_limit: 200`; the everything-at-once test. Difficulty 10.
|
||||
2. Same playability fixtures as task 01 (the parametrized file picks them up automatically) —
|
||||
if the greedy bot cannot finish a level, that is a level bug (fix the level), unless the bot
|
||||
genuinely lacks the mechanics (then say so in the report and add the missing bot behaviour to
|
||||
`tests/sim/bots.py`, which is allowed).
|
||||
3. `dark_kitchen` modifier: server effect is already defined in phase 10 task 04 — verify the
|
||||
client overlay (`render/veil.js`) is driven **only** by the modifier flag in state (no
|
||||
position hiding on the server; this stays a presentation effect, documented).
|
||||
4. `docs/LEVELS.md` sheets for 7–10, plus an "everything the campaign teaches" table mapping
|
||||
each mechanic to the level that introduces it and the level that tests it under pressure.
|
||||
|
||||
## Testing & Quality
|
||||
- Sim playability + validator + curve numbers.
|
||||
- Coverage: **>90%**.
|
||||
- Full suite green.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run undercooked-levels validate` → clean with 10 levels
|
||||
- [ ] `uv run pytest tests/sim/test_level_playability.py -q` → green on all 10 levels, 1/2/4 players
|
||||
- [ ] level 10 is completable by the bot with all four hazard systems active (asserted)
|
||||
- [ ] `docs/LEVELS.md` has the mechanic-introduction table complete for all 10 levels
|
||||
- [ ] full test suite **100% passing**
|
||||
- [ ] README.md/AGENTS.md updated for anything user-visible this task changes (the phase's final pass re-checks both project-wide)
|
||||
- [ ] no behavior change in completed work
|
||||
@@ -0,0 +1,45 @@
|
||||
# Task 03 — Escalation curve gate
|
||||
|
||||
**Phase:** `11_campaign_levels_and_progression` · **Story:** `US-07_campaign_progression`
|
||||
|
||||
## Objective
|
||||
Turn "harder" into a number, and make CI fail if the campaign stops climbing.
|
||||
|
||||
## Work
|
||||
1. Define the composite difficulty score in `tools/curve.py` as an explicit, documented formula
|
||||
over measurable axes (no vibes):
|
||||
- `throughput_demand = target_orders / time_limit` (normalized),
|
||||
- `route_cost` = mean bot path length per served order (cells),
|
||||
- `contention` = `contest_losses + lock_waits` per 100 ticks from the bot run,
|
||||
- `pipeline_depth` = mean recipe steps across the `order_book`,
|
||||
- `hazard_load` = weighted count of active hazard systems × their intensity fields,
|
||||
- `dirty_pressure` = sink round-trips per served order,
|
||||
- `score = Σ w_i · axis_i` with the weights declared once in `tools/curve.py` (and mirrored in
|
||||
`docs/DESIGN.md`), so a weight change is a deliberate, reviewable edit.
|
||||
2. `tests/sim/test_escalation_curve.py`:
|
||||
- loads the manifest, runs the bot at 2 players for every level, computes the axes,
|
||||
asserts the composite is non-decreasing across `difficulty` order, with ≥ 7 of 9 strict
|
||||
increases (equal scores allowed only for adjacent levels explicitly annotated
|
||||
`difficulty_tie: <reason>` in the YAML — the annotation is validated too),
|
||||
- asserts `stars` thresholds are strictly increasing per level and that 3 stars requires
|
||||
beating `target_orders` by ≥ the documented margin,
|
||||
- prints a table on failure (so a balance regression names the offending axis).
|
||||
3. `tools/curve.py --report` writes the current table to `docs/LEVELS.md`'s metrics section
|
||||
between `<!-- curve:begin -->` / `<!-- curve:end -->` markers; `tests/unit/test_docs_index.py`
|
||||
extended to assert the markers exist and the table is not stale (regenerating produces
|
||||
identical bytes).
|
||||
4. Fix any level the gate flags by editing YAML, not the weights (weights move only with a
|
||||
documented rationale in `docs/DESIGN.md`).
|
||||
|
||||
## Testing & Quality
|
||||
- The gate itself is the deliverable; runtime < 120 s (mark `slow`, run in `validate.sh`).
|
||||
- Coverage: **>90%** on `tools/curve.py`.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest -m slow tests/sim/test_escalation_curve.py -q` → green
|
||||
- [ ] temporarily editing one level's `time_limit` to flatten the curve makes the test fail and names the axis (demonstrated, then reverted)
|
||||
- [ ] `uv run python tools/curve.py --report` is idempotent (byte-identical on a second run)
|
||||
- [ ] weights and axes documented in `docs/DESIGN.md` with the rationale for each
|
||||
- [ ] full test suite **100% passing**, coverage >90%
|
||||
- [ ] README.md and AGENTS.md are accurate and consistent with the whole project (verified in the final pass)
|
||||
- [ ] no behavior change in completed phases
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
# Task 04 — Campaign flow, unlock gating, and the level picker
|
||||
|
||||
**Phase:** `11_campaign_levels_and_progression` · **Story:** `US-07_campaign_progression`
|
||||
|
||||
## Objective
|
||||
Make progression real and tamper-proof: the server decides what is unlocked, the picker shows
|
||||
it, and finishing a level offers the right next step.
|
||||
|
||||
## Work
|
||||
1. Server (`rooms/session.py`, `server/rooms.py`):
|
||||
- `unlocked_levels(session_id)` derived from `best_scores` (a level unlocks when the previous
|
||||
one is completed at ≥ 1 star) with `levels/campaign.yaml` order as the source of truth;
|
||||
- `set_level` **and** `start` reject a level the starting player's session has not unlocked
|
||||
→ `err(level_locked)` with the payload naming the highest unlocked id; spectating/host
|
||||
changes never bypass it, and a room whose members have different progress uses the **host's**
|
||||
progress (documented, and shown in the picker as "host progress");
|
||||
- `/api/session` gains `unlocked: [...]`, `next_level`, `campaign_complete: bool`;
|
||||
- `result` payload gains `unlocked_next` (`id | null`) and `campaign_complete`.
|
||||
2. Client:
|
||||
- level picker: locked levels visible-but-dimmed with the unlock condition as a tooltip,
|
||||
best score + stars per level, `difficulty` pips, and the `blurb`; keyboard navigable,
|
||||
`aria-disabled` rather than hidden;
|
||||
- result screen: `Next level` only when `unlocked_next`, focus moves to the primary action,
|
||||
`Retry` keeps the room and level, `Back to lobby` returns to `LOBBY`, and a
|
||||
`campaign_complete` celebration panel (non-blocking, dismissible, announced);
|
||||
- "host progress" badge plus a note when your own progress differs.
|
||||
3. `tests/server/test_unlock_gating.py` — unlock derivation, `set_level`/`start` rejection with
|
||||
the exact error code, host-progress rule, `campaign_complete`, a spoofed `set_level` for a
|
||||
locked id over a real socket refused.
|
||||
4. `tests/e2e/test_us07_picker.py` — locked level not startable (control disabled + server
|
||||
refusal if forced), best score/stars render, `Next level` appears only when unlocked,
|
||||
keyboard-only navigation reaches every control, screen-reader text present for locked state.
|
||||
|
||||
## Testing & Quality
|
||||
- Server + e2e; the spoofed-locked-level test is the security-relevant one.
|
||||
- Coverage: **>90%** on new/modified code.
|
||||
- Full suite green.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/server/test_unlock_gating.py tests/e2e/test_us07_picker.py -q` → green
|
||||
- [ ] forcing a locked `set_level` over the wire is refused with `level_locked` (asserted)
|
||||
- [ ] `Next level` / `Retry` / `Back to lobby` behave correctly and are keyboard reachable
|
||||
- [ ] campaign completion state is visible and announced once
|
||||
- [ ] full test suite **100% passing**
|
||||
- [ ] README.md/AGENTS.md updated for anything user-visible this task changes (the phase's final pass re-checks both project-wide)
|
||||
- [ ] no behavior change in completed work
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
# Task 05 — Full campaign playthrough suite
|
||||
|
||||
**Phase:** `11_campaign_levels_and_progression` · **Story:** `US-07_campaign_progression`
|
||||
|
||||
## Objective
|
||||
One suite that plays the whole campaign start to finish and proves the progression story holds
|
||||
end to end, in a browser, with real sockets.
|
||||
|
||||
## Work
|
||||
1. `tests/e2e/test_us07_campaign_progression.py` (marked `slow`):
|
||||
- a single browser context + session token plays levels 1→10 via the bot driver
|
||||
(`tests/e2e/bot_driver.py`: reuses the sim bot, replaying its input intents through the
|
||||
real input channel rather than poking the server), finishing each level, then clicking
|
||||
`Next level`;
|
||||
- asserts after every level: result screen shows stars and best score, the next level became
|
||||
unlocked (picker state + `/api/session`), `unlocked_next` matches, and the session record
|
||||
advanced;
|
||||
- asserts a retry after a "lost" round (forced by a test-only `?fail=1` flag that makes the
|
||||
bot idle) does not lose progress and does not re-unlock anything;
|
||||
- asserts mid-campaign disconnect (socket kill between levels) preserves progress on rejoin;
|
||||
- asserts a second browser context with a fresh token sees level 1 only, and that the two
|
||||
sessions do not leak into each other;
|
||||
- finishes the campaign and asserts the completion panel + `campaign_complete` in
|
||||
`/api/session`.
|
||||
2. Runtime discipline: the suite uses the test clock (`UNDERCOOKED_TIME_SCALE=8`, honored only
|
||||
when `UNDERCOOKED_TEST_MODE=1`) so 10 levels run in < 4 minutes; if `TIME_SCALE` does not
|
||||
exist yet, implement it as a room-level tick-source change (the sim's `STEP_DT` never
|
||||
changes — only how fast ticks are pulled), documented in `docs/SIMULATION.md`, with unit
|
||||
tests that a scaled round produces identical state to an unscaled one.
|
||||
3. `tests/server/test_time_scale.py` — scaling changes wall-clock pacing only: same input
|
||||
script, same tick count, same final hash, half the wall time (± tolerance).
|
||||
4. `tests/e2e/README.md` — how to run just the campaign suite, and how to shorten it
|
||||
(`-k "level_0"` for the first three).
|
||||
|
||||
## Testing & Quality
|
||||
- E2E `slow` campaign suite + time-scale unit/server tests; the whole thing must be deterministic
|
||||
(fixed seeds, no wall-clock assertions tighter than 3× tolerance).
|
||||
- Coverage: **>90%** on new/modified code.
|
||||
- Full suite green.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest -m slow tests/e2e/test_us07_campaign_progression.py -q` → green in < 6 minutes
|
||||
- [ ] `uv run pytest tests/server/test_time_scale.py -q` → green with identical final state hash scaled vs unscaled
|
||||
- [ ] progress survives a between-levels socket kill (asserted)
|
||||
- [ ] a fresh session sees only level 1 (asserted), and sessions are isolated
|
||||
- [ ] full test suite **100% passing**, coverage >90%
|
||||
- [ ] README.md and AGENTS.md are accurate and consistent with the whole project (verified in the final pass)
|
||||
- [ ] no behavior change in completed phases
|
||||
Reference in New Issue
Block a user