Files
undercooked/.agents/PLAN.md
T
ducoterra f84c6ebaef 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
2026-10-05 17:05:56 -04:00

30 KiB
Raw Blame History

PLAN.md — Undercooked (master design, LOCKED)

Type: real-time multiplayer web game (in-browser, cooperative) Working title: Undercooked — an independent Overcooked-style homage Status: design locked, implementation begins at phase 01


1. Vision

One browser tab is enough to start playing: open the site, create a lobby, read the 5-character code out loud to your friends, hit Start, and cook together. No signup, no download, no install, no email. Each player's phone or laptop is one cook; the kitchen is shared and authoritative on the server. A campaign of ~10 hand-authored levels ratchets the pressure — more recipes, fewer stations, hostile floors — and an endless mode keeps the escalation going after the campaign ends.

The one thing a user does on day one: create/join a lobby by code and finish level 1 with 2+ other people, seeing each other's cooks move with no perceptible lag.

Success criteria (v1)

  1. 4 players on 4 devices complete level 1 of a lobby in one sitting, no page reloads.
  2. Creating a lobby, sharing the code, and joining takes < 20 seconds end to end.
  3. A player who closes their laptop and returns mid-round rejoins and keeps playing.
  4. All 10 campaign levels load, validate as solvable, and are measurably harder than their predecessor by the difficulty metrics in §8.
  5. Full test suite passes; the game survives a 200-room stress run on one node.

Non-goals for v1

Accounts, auth, payments, persistent accounts/friend lists, matchmaking with strangers, cross-node scaling, mobile-native apps, voice/chat (text-free emotes only), level editor UI, gamepads, internationalization, ranked/online leaderboards.


2. Assumptions

  1. Sessions are short (a round is 60–180 s) and rooms are small (≤ 4 players), so a single asyncio process can hold thousands of rooms with a 20 Hz tick each.
  2. Anonymous is a feature, not a fallback. Losing progress on server restart is acceptable; therefore no database is required (user-approved, §3).
  3. Browser is the only client. Desktop keyboard and touch are both first-class; the client must degrade gracefully on a 360 px-wide phone.
  4. Art is ours. Simple vector shapes/sprites generated in-repo; no third-party asset packs, no CDN fonts.
  5. Trust model: players are guests on our server, potentially hostile. The server validates everything; nothing client-reported is authoritative.
  6. Python is fast enough for a 20 Hz tick over ≤ 4 players + ≤ 60 entities (≈ 0.5–2 ms CPU per room per tick). The perf budget in §10 protects this; if a room misses budget, it drops to a lower tick rate rather than changing the stack.

3. Architectural Anchors (LOCKED DECISIONS)

# Component Decision Rationale Status
A1 Language / tooling Python ≥ 3.12, uv for env + run Project standard; uv sync/uv run everywhere LOCKED
A2 Web framework FastAPI + uvicorn (uvicorn[standard]) Async-native, first-class WebSocket support, Pydantic integration LOCKED
A3 Realtime transport Native WebSocket (/ws/{room_code}), JSON frames Lowest-latency simple option; no polling fallback needed for a game. Socket.IO rejected: extra abstraction, no benefit LOCKED
A4 Simulation authority Server-authoritative fixed-tick sim at 20 Hz; snapshots out, inputs in; client interpolation + local prediction Single source of truth (anti-cheat, no drift), reconnectable, testable headless LOCKED
A5 Sim purity src/undercooked/sim/ has zero I/O, asyncio, wall-clock, or unseeded random; step(state, dt) is a pure function Enables headless determinism tests and frame-rate-independent logic LOCKED
A6 Persistence None. Rooms live in the supervisor's memory; results/session scoreboards in memory. No Postgres, no SQLite, no ORM, no Alembic, no Redis Anonymous, ephemeral by design; user explicitly chose this over the default Postgres rigour LOCKED (user-approved deviation)
A7 Cache / queue None (single process, in-process asyncio tasks) Multi-node fan-out is a v2 concern; adding Valkey now buys nothing LOCKED
A8 Content format Data-driven levels + recipes as YAML in levels/, parsed into Pydantic models and validated at startup No hard-coded content lists; new levels require no Python change LOCKED
A9 Client rendering Canvas 2D, requestAnimationFrame, DPR-aware Smooth motion between snapshots; DOM tiles stutter with many moving entities LOCKED
A10 Client code Vanilla ES modules, no bundler, no framework Zero build step, debuggable in the browser, satisfies the no-CDN rule trivially LOCKED
A11 Art In-repo generated SVG tileset/sprites + CSS; system font stack Portable, tiny, no external fonts/CDN LOCKED
A12 No external CDNs Every asset served from app/static/; CSP default-src 'self' ws: wss:; enforced by a test Project rule; also removes a privacy and availability risk LOCKED
A13 Test stack pytest (+pytest-asyncio) for unit/integration/sim; Playwright for E2E, one suite per user story, multi-context for multiplayer Stories are the delivery unit; two browser contexts = two real players LOCKED
A14 Protocol Versioned JSON messages (p protocol int, type discriminator) documented in docs/PROTOCOL.md; server rejects unknown versions/major mismatch Debuggable, testable, forward-compatible LOCKED
A15 Lobby codes 5 chars, Crockford base32 (no I L O U), server-generated, collision-checked, ~33 M space Shoutable out loud, quick to type on a phone LOCKED
A16 Player identity secrets.token_urlsafe(24) token per browser tab in sessionStorage; display name optional, sanitized, non-unique Anonymous, rejoinable, no PII LOCKED
A17 Config Env vars (UNDERCOOKED_*) + .env via python-dotenv; typed config.py; no hard-coded paths/ports Deployable, testable LOCKED
A18 Debugging debugpy gated by DEBUGPY=1 (non-blocking listen on DEBUGPY_PORT) Real-time server is hard to debug by printing LOCKED
A19 Packaging Multi-stage Containerfile (slim runtime, non-root) + compose.yaml (app only, no DB) Single-artifact deploy LOCKED
A20 Quality gate ruff + pyright + pytest with >90% coverage on new/changed code; full suite green before any phase completes Project standard LOCKED
A21 Determinism harness Input scripts + seed replay to a golden state hash (tests/sim/test_replay_*.py) Proves A4/A5 and makes balance regressions cheap to catch LOCKED
A22 Lint/format targets pyproject.toml [tool.ruff], [tool.pyright], [tool.pytest.ini_options] as scaffolded One source of truth for checks LOCKED

Anything not in this table is fair game to choose within an existing anchor. Using a new dependency, service, protocol, or framework requires explicit user permission, recorded in the phase's 00_phase.md and appended to this table by the user.


4. High-level architecture

            browser (player)                        server (one asyncio process)
 ┌───────────────────────────────────┐   WS    ┌──────────────────────────────────────┐
 │ app/static/js                      │         │ server/app.py         FastAPI app     │
 │  app.js        screen state mach.  │═══════▶ │  ├─ /            index.html          │
 │  net/connection.js  WS + backoff   │  JSON   │  ├─ /static/*    ES modules, SVG     │
 │  net/predict.js     local predict  │◀═══════ │  ├─ /healthz /api/levels /api/rooms  │
 │  net/snapshots.js   buffer+lerp    │ frames  │  └─ /ws/{code}   ws endpoint         │
 │  input/keyboard.js|touch.js        │         │        │                             │
 │  render/canvas.js  layers, camera  │         │ server/protocol.py  msg models+valid. │
 │  render/hud.js     score/time/orders│        │ server/room.py      Room + FSM       │
 │  ui/…              lobby/result DOM│         │        │ 20 Hz tick task             │
 └───────────────────────────────────┘         │ server/rooms.py     RoomSupervisor   │
                                               │ server/sessions.py  in-memory runs   │
                                               │        │                             │
                                               │ sim/*  pure simulation kernel        │
                                               │ levels/*.yaml → sim/level.py         │
                                               └──────────────────────────────────────┘
                                                       (no database, no external calls)

Component responsibilities

Component Owns Never does
sim/ game truth: state, step(), interactions, scoring, hazards network, HTTP, time, randomness outside round seed
sim/level.py + sim/validation.py YAML → typed LevelDef; reachability/solvability validation simulation stepping
server/room.py one match: player seats, FSM, tick loop, snapshot/event emission, rejoin seats routing, HTTP, cross-room logic
server/rooms.py room registry, code issuance/collision, capacity, GC of idle rooms game rules
server/protocol.py message models, per-state whitelists, inbound validation, framing caps game rules
server/sessions.py in-memory records: completed rounds, best scores per session token durable storage
server/app.py FastAPI factory, static files, headers/CSP, startup level validation game rules
client net/ connection lifecycle, snapshot buffer, interpolation, prediction/reconciliation deciding anything
client render/ drawing, camera, HUD, effects mutating game state

Data flow (one tick, 20 Hz)

  1. Each connected client sends at most one input per frame: {seq, move:{x,y}, act, face} — only on change, plus a keep-alive at ≥ 5 Hz.
  2. protocol.py validates shape, size, rate, and legality for the room's state; stale seq is dropped; illegal moves are clamped, never trusted.
  3. room.tick() applies queued inputs to the sim, then sim.step(state, dt) advances physics, station timers, order spawning, hazards, and win/loss checks.
  4. The sim returns (state, events); events are transient FX/log lines (chopped, burnt, served, order_missed, fire_started, score, …).
  5. The room builds a snapshot (compacted: entities quantized to 1/64 tile, station arrays, order list, score/timer) and pushes snap frames to players at SNAPSHOT_HZ and to spectators at half that; events ride along with the frame.
  6. Client buffers snapshots and renders at now − 100 ms, interpolating between the two bracketing snapshots; own player is additionally dead-reckoned from local input and reconciled when a snapshot contradicts it.

Snapshot content (contract, compact)

{ "t": 4821, "p": [ { "i":"ab12", "x":12.34, "y":8.06, "f":2, "h":"tomato_chopped",
                     "s":"move", "pi":0 } ],
  "st":[ {"k":"chop","x":3,"y":2,"g":"onion","q":6,"p":0.42,"s":1} ],
  "o": [ {"id":17,"r":"soup","l":0.63,"pts":40} ],
  "s": 120, "tgt": 140, "tl": 83.65, "hz": [ {"t":"fire","x":7,"y":5,"l":0.8} ],
  "e": [ {"k":"served","x":9,"y":1,"i":17} ] }

Quantization: positions are floats rounded to 2 decimals on the wire, progress to 2 decimals — enough for smooth interpolation, small enough to keep frames ≈ 300–900 B.


5. Simulation model (pure)

Entity/state types (sim/model.py)

  • Vec2, Facing (up/right/down/left), TileKind (FLOOR, COUNTER, WALL, VOID)
  • StationKind (SPAWN_SHELF, CHOP_BOARD, STOVE, POT, PREP, SERVE, SINK, TRASH)
  • Station — position, kind, contents (list[Item]), progress, state (idle|working|ready|burning|dirty|blocked), per-station cooldown
  • Item — kind (tomato|onion|lettuce|dough|patty|rice|fish|…), form (raw|chopped|cooking|cooked|burnt|plated|dirty), quality (1.0 → 0.0 as it burns), ttl
  • Player — id, pos, vel, facing, carrying (Item|None), action state, state (idle|moving|using|stunned), stun, score_share, connected
  • Order — id, recipe id, deadline, time_left, value
  • Hazard — fire|spill|moving_obstacle, pos, life, path/period for movers
  • KitchenState — level id, seed, tick, grid, stations, players, items, orders, hazards, score, target, time_left, result (None|won|lost), counters, rng state

Movement

Continuous positions in tile units, velocity from the input vector, radius 0.35 tile, axis-separated AABB resolution against blocked tiles, other players (soft push, mass 1), and carried items. Max speed 3.5 tiles/s, accel 30 tiles/s², no diagonal speed boost. Void tiles are lethal to movement (blocked, not falling — v1 keeps it simple); a moving_obstacle occupies tiles it passes over and stuns on contact (0.6 s).

Interaction model

One action button. interact(state, player) resolves against, in priority order:

  1. carrying → drop/transfer onto target station (throwing is out of scope v1),
  2. station in reach (1 tile in front, then radius 0.6): pick item, start work, place item, plate, serve, wash, trash (long-press),
  3. nothing → short "empty" event (client plays a whiff).

Two-hand rules: one carried item per player. Chopping requires holding act for the chop duration; taking a chopped item off the board requires the board to be ready. Contested targets resolve by earliest server-received input, tie-broken by player seat index; the loser gets denied (client shows a nudge) — never a silent no-op.

Station pipelines

Station Input Progress Output
Spawn shelf — instant, infinite while orders need it raw item
Chop board raw hold act ~0.9 s chopped (rot raw in 20 s)
Stove + pot chopped (soup) / raw (rice) 4–8 s cooked → burnt after 8 s idle
Pan/griddle raw patty/fish 5 s cooked → burnt; can start fire
Prep/assemble cooked/chopped + plate per recipe plated dish
Serve window plated instant match vs orders score, quality bonus, streak
Sink dirty plate/pot 1.2 s clean (dirty stock caps available plates/pots)
Trash any item instant gone (small score nudge −2)

Orders, scoring, results

  • Orders spawn from the level's pool with concurrent and spawn_interval bounds.
  • Serving a correct dish: base × quality × streak_mult × time_bonus. Wrong dish: rejection event, no score, order stays.
  • Missed order (deadline 0): −order_penalty, streak reset, client shake.
  • target_score reached → result=won immediately (with stars from remaining time); time_left == 0 → won if score ≥ target else lost.
  • Per-player contribution is tracked for the result screen (display only, never competitive gatekeeping).

6. Lobby, rooms, and state machines

Room FSM (server/room.py)

LOBBY ──(host starts, ≥1 ready)──▶ COUNTDOWN(3s) ──▶ PLAYING
  ▲                                                    │
  │                                          result=won|lost|aborted
  │  next-level / retry                                ▼
  └──────────────(≤ 20 s auto, any host action)── ROUND_END
       │ ROUND_END ──(idle > lobby_idle_timeout)──▶ CLOSED (GC, seats freed)
       └ LOBBY ──(all seats empty > grace)────────▶ CLOSED
  • LOBBY: joinable/leavable, ready flags, level pick (host; limited to unlocked), chat-free emotes only.
  • COUNTDOWN: 3…2…1 broadcast; late joins wait in SPECTATING until LOBBY.
  • PLAYING: inputs accepted from seated players; joiners become spectators; dropouts keep their seat (connected=false, sim keeps them idle 3 s then parks them).
  • ROUND_END: results, unlocked-next-level state, retry/next/host-picked level.
  • CLOSED: supervisor drops the room; tokens become invalid.

Supervisor rules

  • Codes: Crockford base32, 5 chars, retried on collision; never reused within the process; the empty-lobby room is created on click, not on page load.
  • Caps: max_players (4) per room, max_rooms (200) process-wide → 503 room_full.
  • GC task every 5 s: close idle lobbies, evict orphaned rooms, purge stale seats.
  • Every room owns exactly one asyncio tick task; task cancellation is the only teardown.
  • Crash isolation: a room's tick exception is caught, logged, the room is marked aborted, players are told, and the rest of the server is unaffected.

Session records (server/sessions.py, in memory, bounded)

session_token → {display_name, unlocked_level, best[ level_id ] = {score, stars, completed_at}, rounds_played}. LRU-capped at 10 000 entries; wiped on process restart (acceptable per A6).


7. Wire protocol (docs/PROTOCOL.md)

Protocol version p: 1. Server closes the socket with code 4400 on major mismatch.

Direction type Payload Accepted when
C→S hello {token?, name?, p} connecting
C→S ready {} LOBBY
C→S set_level {level} LOBBY, host only, level unlocked
C→S start {} LOBBY, host only
C→S input {seq, move:{x,y}, act, face} PLAYING
C→S emote {id} LOBBY, PLAYING
C→S to_lobby / next_level {} ROUND_END
C→S ping {t} always
S→C welcome {you, room, state, level, seats} after hello
S→C lobby {seats[], level, host, can_start} lobby-state changes
S→C countdown {n} COUNTDOWN
S→C snap snapshot (§4) PLAYING
S→C round {result, score, target, stars, unlocked, players[]} ROUND_END
S→C ev [{k, …}] (FX/log) any
S→C err {code, msg, seq?} on rejection
S→C pong {t, now} after ping

Inbound guards: max max_message_bytes; JSON-only; unknown type → err unknown_type (disconnect after 5); per-state whitelist; per-connection token bucket max_inputs_per_s; name sanitization (strip control chars, trim, 16 chars); one socket per token (second connection evicts the first).


8. Level content and the difficulty curve

levels/recipes.yaml defines ingredients and recipes; levels/NN_*.yaml defines each level. Every list the game reads comes from these files — the code never enumerates levels or recipes (validated by a test that adds a fixture level and expects it to appear in /api/levels).

id: 07_riverboat
name: Riverboat Galley
order: 7
time_limit: 100
target_score: 150
grid:                       # ASCII, one char per tile; '#'=wall '.'=floor …
  - "##############"
  - "#S..C..P....X#"
  - "#..==..==....#"
  - "#P1.2......ZZ#"
tiles: { "S": {kind: spawn_shelf, item: tomato}, "C": {kind: chop_board}, … }
spawns: [{x: 1, y: 9}, {x: 12, y: 9}]
orders:
  pool: [soup, salad, fried_rice]
  concurrent: 3
  spawn_interval: [7, 11]
  deadline: 42
hazards:
  - {type: fire, station: stove_2, chance_per_s: 0.02, extinguish: sink}
  - {type: spill, x: 6, y: 5, respawn_s: 25}
  - {type: moving_obstacle, path: [[4,3],[10,3]], period_s: 9}
station_limits: {plates: 3, pots: 2}
escalation_notes: "splits the kitchen; fire pressure without an extinguish shortcut"

Escalation axes (each campaign level must move ≥ 2 of them vs. its predecessor)

  1. Recipe complexity — 1 → 4 ingredients, multi-station chains.
  2. Order pressure — concurrent 1 → 4, spawn_interval 16 s → 6 s, deadline 60 → 30 s.
  3. Time budget — time_limit 150 → 75 s against rising target_score.
  4. Station scarcity — 1 shared stove for 4 players, limited plates/pots (dirty loop).
  5. Layout hostility — open room → two halves → narrow bridges → void gaps.
  6. Hazards — none → spills → fires → moving obstacles → order-spam "rush" waves.
  7. Distance — ingredient ↔ cook ↔ serve travel 3 → 14 tiles.

Campaign: L1 tutorial (solo-friendly), L2 two orders, L3 chop+cook chain, L4 dirty-dish loop, L5 split kitchen, L6 fire, L7 boat + movers, L8 bridge/void, L9 rush waves, L10 gauntlet (all axes). Then endless: generated by sampling a difficulty scalar d = 10 + rounds against the same axes over the authored layouts.

Validation gate (undercooked-levels validate, also a test over every shipped level): ids unique; grid rectangular, spawns/stations on floor; every spawn, station, and serve window mutually reachable (BFS on walkable tiles, movers excluded); every recipe's ingredient reachable and every required station kind present; time_limit > 0; target_score ≤ theoretical_max_score × 0.6 (greedy plausibility: orders possible in the time window × mean order value); hazards reference existing stations; escalation axes actually increase between consecutive order values.


9. Validation / verification workflow (how we know it is right)

Layered, cheapest first — every phase runs the layers it touched, and the pipeline gate runs all of them:

  1. Static: ruff check (lint incl. ASYNC, S, PERF), pyright.
  2. Pure-sim unit tests: movement/collision, station state machines, recipe/serve resolution, scoring, hazards, validation rules — table-driven, no I/O.
  3. Determinism/replay: seeded input script → N ticks → state hash compared to a golden file; a second run after a refactor must match bit-for-bit.
  4. Room/protocol integration: in-process FastAPI + real WebSocket clients — create/join/ready/start/snap/round/rejoin, illegal-message rejection, rate limiting, 4-player full round, room GC.
  5. Content gate: undercooked-levels validate over every YAML (fixture + shipped).
  6. E2E per user story (Playwright): real browser, real sockets; multiplayer stories drive two browser contexts against the same room and assert both observe the same outcome (score, result, seats).
  7. Netcode tests: server-side latency injection (UNDERCOOKED_SIM_LATENCY_MS=150), socket-kill mid-round + rejoin, prediction error bound assertions.
  8. Scale/perf budget (marked slow): 200 rooms × 4 clients ticking 60 s — mean tick ≤ 2 ms/room, RSS growth < 20 %, zero missed ticks > 50 ms.
  9. Doc & a11y pass: README/AGENTS reviewed project-wide; axe-core-free checks (contrast, focus order, aria-live result announcements) in the a11y story's suite.

A phase is Complete only when layers 1–5 relevant to its change pass and the entire suite is green with >90% coverage on new/modified code.


10. Performance and reliability budgets

Budget Target How measured
Server tick cost mean ≤ 2 ms, p99 ≤ 8 ms (4 players) room.tick timing histogram, stress test
Snapshot size ≤ 1.5 KB JSON encoded-size assert in integration test
Downlink rate ≤ 60 KB/s per player measured in integration test
Client frame time ≤ 16 ms at 4 players on a mid-tier laptop Playwright performance.now() sampling
Input → visible echo ≤ 120 ms (incl. 100 ms interpolation) netcode test
Room memory ≤ 1 MB per active room RSS delta in stress test
Room capacity ≥ 200 concurrent rooms stress test
Rejoin to playable ≤ 3 s after page load e2e test

Degradation policy: if a tick exceeds budget, halve that room's effective tick rate for 5 s and emit ev{kind:"tick_slow"}; never drop players for lagging.


11. Security & privacy posture

  • No PII collected; no cookies; no localStorage beyond the session token and last room code; no third-party requests at runtime (test-enforced).
  • WebSocket: origin check (ALLOWED_ORIGINS, default same-host), message-size and rate limits, per-state whitelists, one socket per token, idle close.
  • Room codes are unguessable-ish but are capability secrets: never logged with contents, and every server-side check assumes the code holder is a legitimate player and validates their actions, not their identity.
  • Static serving: no directory listing, path traversal guarded by StaticFiles, X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer, CSP default-src 'self'; connect-src 'self' ws: wss:; img-src 'self' data:; style-src 'self'.
  • No eval, no dynamic JS assembly from server strings, no user HTML rendering (names are rendered via textContent).

12. Roadmap (phases; details in .agents/phases/todo/)

# Phase Delivers (player-visible if shipped)
01 runnable_service_scaffold uv run undercooked serves the shell page + /healthz; checks green
02 level_content_and_validation level/recipe schema, loader, validator, 2 levels, undercooked-levels validate
03 simulation_movement_and_interaction deterministic movement, reach, carry; replay hashes pinned
04 kitchen_pipeline_and_scoring full cook chain, orders, scoring, win/lose; headless round sim
05 anonymous_lobbies_and_room_lifecycle supervisor, codes, ready-up, round FSM, session progression
06 realtime_websocket_protocol /ws/{code}, inputs in, snapshots out, docs/PROTOCOL.md
07 canvas_client_and_hud playable-in-browser single player vs. clock, HUD, art
08 client_prediction_and_touch smooth remote play, prediction/reconciliation, touch controls
09 co_op_two_player_e2e two real browsers cooking together, contested-resource arbitration
10 hazards_and_escalation_mechanics fire, spills, movers, rush waves + client FX
11 campaign_levels_and_progression all 10 levels authored+validated, campaign flow, curve check
12 reconnect_resilience_and_scale mid-round rejoin, seat grace, room GC, 200-room stress test
13 endless_mode_and_scoreboard endless generator, session scoreboard, post-campaign loop
14 accessibility_polish_and_hardening contrast/reduced-motion/screen-reader, perf+security pass, docs

Every phase leaves the app launchable and the suite green; phases 01→14 are strictly sequential (each depends on the previous phase's capability).


13. Risks and mitigations

Risk Impact Mitigation
Python tick too slow at scale laggy rooms perf budget test in phase 12 from day one; sim purity keeps the hot loop cheap; halve-tick degradation
Client desync/rubber-banding feels bad unplayable server-authoritative + interpolation delay + reconciliation tests (phase 08)
Contested pickups feel unfair co-op friction deterministic arbitration + explicit denied feedback (phase 09)
Difficulty curve guesswork too easy/hard escalation-axis metrics + headless greedy solver bound (phase 11)
Level content explodes in complexity unmaintainable strict YAML schema + validator + content test that never hard-codes lists
Anonymous rooms abused (spam) noisy lobbies no chat in v1, emote whitelist, rate limits
Restart wipes runs lost progress accepted by design (A6); result screen encourages saving nothing; endless/scoreboard explicitly in-memory
Trademark confusion with "Overcooked" legal original art, names, and content; homage note in README

Documentation index (canonical)

These are the only docs the project promises. A phase creates a file the first time it needs one; no phase references a doc that does not exist by the end of the last phase.

Path Contents Created by
README.md what it is, quickstart, how to play, controls, deploy pointer, demo-vs-production phase 01, refined throughout
AGENTS.md agent rules: commands, architecture, conventions, guardrails phase 01, verified every phase
BUDGETS.md measured performance/latency budgets vs §10 targets, dated phase 08
docs/DESIGN.md architecture, module map, the decisions behind A1–A12 in prose phase 01 (stub) → phase 09
docs/SIMULATION.md tick contract, movement/collision, pipeline rules, arbitration, hazard rules phase 02/03
docs/LEVELS.md level + recipe YAML format, escalation axes, how to author and validate a level phase 02, phase 11
docs/PROTOCOL.md wire messages, enums, error codes, close codes, budgets, versioning policy phase 06
docs/CLIENT.md client module map, render loop, snapshot buffer, input, assets phase 07
docs/DEPLOYMENT.md container build/run, compose, TLS/reverse proxy, ops runbook phase 01 (stub) → phase 14
docs/TESTING.md test layout, how to run everything, artifact policy phase 01
docs/ACCESSIBILITY.md what WCAG 2.1 AA guarantees, how each is tested, text-mode contract, known limits phase 14
docs/SECURITY.md threat model + finding table with severity, status, and the evidence test per row phase 14
tests/e2e/README.md multiplayer harness usage, debugging failing browser tests phase 07

tests/unit/test_docs_index.py (phase 01) keeps this table and the docs/ tree identical, so neither can drift silently.


14. Execution

  • Run the pipeline with the phased-execution skill (/next-phase, /run-phase, /phase-status); task files in .agents/phases/todo/NN_name/ are the unit of work, and each phase moves to .agents/phases/complete/ only when its 00_phase.md Completion Criteria are all checked.
  • New work is never implemented directly: it is captured as a phase via the phase-authoring skill first, so this roadmap stays the record of what the project claims to do.
  • Amending a LOCKED decision (§3) requires editing §3 itself in the same phase, with the reason and the phase number recorded inline — no silent reversals.