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,25 @@
|
||||
# US-01 — Create an anonymous lobby
|
||||
|
||||
**As a** player who just opened the site,
|
||||
**I want to** press one button and get a room code,
|
||||
**so that** I can start a game with friends without creating an account.
|
||||
|
||||
## Acceptance criteria
|
||||
1. The landing screen shows exactly one primary action: *Create lobby*.
|
||||
2. Creating a lobby issues a 5-character code from the Crockford base32 alphabet
|
||||
(no `I`, `L`, `O`, `U`), uppercase, shown large and with a copy button.
|
||||
3. No account, email, password, cookie consent, or third-party request is involved.
|
||||
4. The browser stores only a random session token (`sessionStorage`) and the room code.
|
||||
5. The creator is marked `host` and holds a seat; the room is in `LOBBY`.
|
||||
6. Reload of the same tab with the same token returns the player to the same seat.
|
||||
7. If the server is at `max_rooms`, the UI shows a clear "lobby full, try again" error
|
||||
instead of a broken screen.
|
||||
8. An idle, empty lobby is garbage-collected.
|
||||
|
||||
## E2E suite
|
||||
`tests/e2e/test_us01_create_lobby.py` — load `/`, create lobby, assert code shape,
|
||||
reload → same seat, assert zero non-origin network requests.
|
||||
|
||||
## Phases
|
||||
`01_runnable_service_scaffold`, `05_anonymous_lobbies_and_room_lifecycle`,
|
||||
`07_canvas_client_and_hud`
|
||||
@@ -0,0 +1,29 @@
|
||||
# US-02 — Join a lobby with a code
|
||||
|
||||
**As a** friend who was told a 5-character code,
|
||||
**I want to** type it on my own device and be in the kitchen,
|
||||
**so that** we can play together from anywhere.
|
||||
|
||||
## Acceptance criteria
|
||||
1. The landing screen has a *Join* field accepting the code, case-insensitive,
|
||||
tolerating whitespace and `l`/`1`-style misreads for excluded characters.
|
||||
2. Joining with a valid code puts the player in a seat with a generated display name
|
||||
(`Cook #2`) if none was typed, and broadcasts a `lobby` update to everyone.
|
||||
3. Joining a full room shows "this kitchen is full (4/4)" and the room code is kept
|
||||
in the field for a retry.
|
||||
4. Joining an unknown code shows "no kitchen with that code" (no leak of which
|
||||
part failed) and never reveals whether a room exists behind a closed door.
|
||||
5. Joining during `PLAYING` puts the player in `SPECTATING` (visible "watching"
|
||||
banner) until the next `LOBBY`.
|
||||
6. A display name is sanitized (control chars stripped, 16-char cap) and is not
|
||||
unique-enforced; two players may share a name.
|
||||
7. The second socket from the same token evicts the first cleanly (no ghost).
|
||||
8. Join → seated takes < 1 s on localhost.
|
||||
|
||||
## E2E suite
|
||||
`tests/e2e/test_us02_join_by_code.py` — two browser contexts: create in A, join in B,
|
||||
assert both see 2 seats; full-room and bad-code paths.
|
||||
|
||||
## Phases
|
||||
`05_anonymous_lobbies_and_room_lifecycle`, `06_realtime_websocket_protocol`,
|
||||
`07_canvas_client_and_hud`
|
||||
@@ -0,0 +1,28 @@
|
||||
# US-03 — Ready up and start from the lobby
|
||||
|
||||
**As a** host,
|
||||
**I want to** pick the level and start once everyone is ready,
|
||||
**so that** the group begins together on the same kitchen.
|
||||
|
||||
## Acceptance criteria
|
||||
1. The lobby lists every seat: name, ready state, host crown, connection state.
|
||||
2. Any player can toggle *Ready*; the host can pick any **unlocked** level from a
|
||||
list showing name, target score, time limit, and difficulty pips.
|
||||
3. *Start* is enabled for the host when ≥ 1 non-host player is ready (or the host is
|
||||
alone — solo play is allowed).
|
||||
4. Starting begins a 3-2-1 `countdown` shown identically on all clients, then the
|
||||
first `snap` arrives.
|
||||
5. Non-host players cannot start or change level; the attempt shows a disabled control
|
||||
(no console errors, no silent failure).
|
||||
6. If the host leaves in `LOBBY`, host status transfers to the lowest occupied seat;
|
||||
in `PLAYING` the round continues.
|
||||
7. Someone leaving during `COUNTDOWN` cancels the countdown back to `LOBBY`.
|
||||
8. Starting creates a round record in the in-memory session store.
|
||||
|
||||
## E2E suite
|
||||
`tests/e2e/test_us03_ready_and_start.py` — two contexts: ready toggles visible to
|
||||
both, non-host start rejected, countdown then both screens show the kitchen.
|
||||
|
||||
## Phases
|
||||
`05_anonymous_lobbies_and_room_lifecycle`, `06_realtime_websocket_protocol`,
|
||||
`07_canvas_client_and_hud`
|
||||
@@ -0,0 +1,30 @@
|
||||
# US-04 — Move and cook in the shared kitchen
|
||||
|
||||
**As a** player,
|
||||
**I want to** walk my cook, grab ingredients, chop, cook, plate, serve, and wash,
|
||||
**so that** I can complete orders with my hands.
|
||||
|
||||
## Acceptance criteria
|
||||
1. Keyboard (WASD/arrows) moves the cook smoothly in 8 directions without diagonal
|
||||
speed boost; the cook stops at walls, counters, and other players.
|
||||
2. One action key (`Space`/`Enter`) interacts with the tile(s) in front, choosing the
|
||||
sensible verb: pick up, start/hold work, place, plate, serve, wash, trash.
|
||||
3. Holding the action key performs progressive work (chop ≈ 0.9 s, cook auto-runs,
|
||||
wash ≈ 1.2 s) with a visible progress ring.
|
||||
4. One carried item per player; the carried item is drawn above the cook and its
|
||||
form (`raw`/`chopped`/`cooking`/`cooked`/`burnt`/`plated`) is visually distinct.
|
||||
5. Reach is 1 tile in front (plus a 0.6-tile forgiveness radius); out-of-reach presses
|
||||
give an explicit "move closer" nudge rather than doing nothing.
|
||||
6. A contested target resolves deterministically: one winner, and the loser gets
|
||||
visible `denied` feedback — never two players holding one item.
|
||||
7. Serving a wrong dish is refused with feedback and the order survives.
|
||||
8. The full chain is possible without dead ends: dirty plates/pots return to the sink
|
||||
and come back usable; trash always accepts an item.
|
||||
|
||||
## E2E suite
|
||||
`tests/e2e/test_us04_move_and_cook.py` — scripted keyboard session completes one soup
|
||||
end to end; asserts HUD score bump, station state changes, denied path on a blocked tile.
|
||||
|
||||
## Phases
|
||||
`03_simulation_movement_and_interaction`, `04_kitchen_pipeline_and_scoring`,
|
||||
`07_canvas_client_and_hud`, `08_client_prediction_and_touch`, `09_co_op_two_player_e2e`
|
||||
@@ -0,0 +1,29 @@
|
||||
# US-05 — Orders, score, timer, and the round result
|
||||
|
||||
**As a** player,
|
||||
**I want to** see what orders are pending, how I am scoring, and how much time is left,
|
||||
**so that** I know whether we won or lost and why.
|
||||
|
||||
## Acceptance criteria
|
||||
1. The HUD shows the order queue with recipe composition, a per-order time bar, and the
|
||||
point value; the currently-satisfiable order is visually distinguished.
|
||||
2. Score, target, and countdown timer are always visible and update within 200 ms of the
|
||||
server event.
|
||||
3. Serving a correct dish awards `base × quality × streak × time_bonus`, plays a pop
|
||||
effect, and removes the order; streak is shown from 2 upward.
|
||||
4. A missed order deducts the penalty, resets the streak, and flashes the HUD.
|
||||
5. Reaching `target_score` ends the round immediately as `won`; the timer hitting 0 ends
|
||||
it as `won` only if `score ≥ target`, else `lost`.
|
||||
6. The result screen shows result, stars (1–3), score/target, best score for the level,
|
||||
per-player contribution, and *Retry* / *Next level* / *Back to lobby* actions.
|
||||
7. The result screen is announced to assistive tech (`aria-live`) and is reachable with
|
||||
the keyboard alone.
|
||||
8. The same round driven by the same seed and input script produces the same score
|
||||
(covered by the replay test, not the browser).
|
||||
|
||||
## E2E suite
|
||||
`tests/e2e/test_us05_orders_score_result.py` — serve to win, assert result screen +
|
||||
stars; force a miss, assert penalty and streak reset.
|
||||
|
||||
## Phases
|
||||
`04_kitchen_pipeline_and_scoring`, `07_canvas_client_and_hud`
|
||||
@@ -0,0 +1,32 @@
|
||||
# US-06 — Cook cooperatively with 2–4 players
|
||||
|
||||
**As a** member of a group,
|
||||
**I want to** see the other cooks move in real time and share one kitchen,
|
||||
**so that** we can divide tasks instead of racing for the same knife.
|
||||
|
||||
## Acceptance criteria
|
||||
1. Every client sees every other cook at their server-authoritative position with
|
||||
smooth motion (interpolated), a name label, and a distinct color.
|
||||
2. Remote motion at 150 ms RTT looks continuous: no teleporting, ≤ 0.25 tile visible
|
||||
error while moving, and no rubber-banding of the local cook.
|
||||
3. The local cook responds to input before the server confirms it (prediction) and is
|
||||
corrected gently (≤ 150 ms ease) when the server disagrees.
|
||||
4. Players physically block each other with a soft push (no standing on tiles already
|
||||
occupied). Carrying players is out of scope; passing an item hand-to-hand happens by
|
||||
both cooks using the same counter/prep tile.
|
||||
5. A contested pickup/station gives exactly one winner; the loser sees `denied` and can
|
||||
immediately target the next-best option.
|
||||
6. Occupancy: a station being actively worked shows who is working it; a second player
|
||||
cannot start a parallel job on it.
|
||||
7. All clients converge on the same score and the same result screen within 1 s of the
|
||||
round end.
|
||||
8. 4 players in one room keeps the mean server tick ≤ 2 ms.
|
||||
|
||||
## E2E suite
|
||||
`tests/e2e/test_us06_coop_shared_kitchen.py` — two contexts cooperatively serve one
|
||||
order and both observe the same score/result; contested-pickup arbitration test;
|
||||
4-connection integration test server-side.
|
||||
|
||||
## Phases
|
||||
`06_realtime_websocket_protocol`, `08_client_prediction_and_touch`,
|
||||
`09_co_op_two_player_e2e`
|
||||
@@ -0,0 +1,30 @@
|
||||
# US-07 — Progress through a campaign that gets harder
|
||||
|
||||
**As a** player finishing level after level,
|
||||
**I want to** unlock the next kitchen and feel it get harder,
|
||||
**so that** a session has a trajectory instead of one repeated map.
|
||||
|
||||
## Acceptance criteria
|
||||
1. The level list shows all shipped levels in campaign order with the difficulty pips,
|
||||
target, time limit, and per-level best score/stars.
|
||||
2. Completing a level unlocks the next one **for that session**; the next level is
|
||||
pre-selected in `ROUND_END` and startable by one action.
|
||||
3. Failing a level keeps the same next choice available (retry) and does not unlock
|
||||
forward; unlocking never goes backwards.
|
||||
4. Level content comes from `levels/*.yaml`; adding a new valid YAML file makes the level
|
||||
appear in `/api/levels` with no Python change (regression test).
|
||||
5. Every campaign level passes content validation: reachability, recipe solvability,
|
||||
target plausibility, hazard references.
|
||||
6. Each consecutive level moves at least two escalation axes (§8 of PLAN.md) versus its
|
||||
predecessor; the curve check fails the build otherwise.
|
||||
7. All 10 campaign levels are playable end to end with the intended station set.
|
||||
8. Progression state is per session token and survives a page reload; it is documented
|
||||
as in-memory and lost on server restart.
|
||||
|
||||
## E2E suite
|
||||
`tests/e2e/test_us07_campaign_progression.py` — win level 1, assert level 2 unlocked,
|
||||
selected, and startable; fail a level, assert locked. Plus
|
||||
`tests/unit/test_level_curve.py` for the escalation-axis and validation gates.
|
||||
|
||||
## Phases
|
||||
`02_level_content_and_validation`, `11_campaign_levels_and_progression`
|
||||
@@ -0,0 +1,29 @@
|
||||
# US-08 — Handle hazards and pressure without losing control
|
||||
|
||||
**As a** player in a late level,
|
||||
**I want to** deal with fires, spills, moving obstacles, and order surges,
|
||||
**so that** the escalation is a fair test of teamwork rather than random unfairness.
|
||||
|
||||
## Acceptance criteria
|
||||
1. **Fire** starts on a station per level probability, spreads to an adjacent station
|
||||
after its burn timer unless dealt with, blocks its tile, and ruins items on it; it is
|
||||
extinguished by a sink-filled douse (carry water) with visible steam.
|
||||
2. **Spill** makes its tile slippery (accel/brake reduced, cook slides ~0.5 tile) and is
|
||||
cleaned by a wipe action; a cook leaving the kitchen with a spill under a station
|
||||
does not propagate it.
|
||||
3. **Moving obstacles** (cart, boat mast) follow their authored path and stun on contact
|
||||
for 0.6 s with a visible telegraph 0.5 s before entering a tile.
|
||||
4. **Rush waves** spawn a burst of orders with a HUD "RUSH" banner and a wave counter.
|
||||
5. Every hazard is data-driven (level YAML): type, location/station reference, rate, and
|
||||
mitigation — no hard-coded hazard placement in Python.
|
||||
6. Hazard effects are deterministic under a fixed seed (replay test covers all four).
|
||||
7. Client FX exist for each hazard and respect `prefers-reduced-motion` (reduced: static
|
||||
markers instead of flicker/shake).
|
||||
8. Hazard penalties never soft-lock a round: the round always ends by timer or target.
|
||||
|
||||
## E2E suite
|
||||
`tests/e2e/test_us08_hazards.py` — level fixture with fire + spill: extinguish, clean,
|
||||
avoid a mover; asserts hazard states in the snapshot and reduced-motion rendering.
|
||||
|
||||
## Phases
|
||||
`10_hazards_and_escalation_mechanics`, `11_campaign_levels_and_progression`
|
||||
@@ -0,0 +1,29 @@
|
||||
# US-09 — Drop out and come back mid-round
|
||||
|
||||
**As a** player on flaky Wi-Fi (or a locked laptop),
|
||||
**I want to** reconnect and land back in the same kitchen mid-round,
|
||||
**so that** one dropped socket does not ruin the group's game.
|
||||
|
||||
## Acceptance criteria
|
||||
1. Losing the socket keeps the player's seat for `GUEST_IDLE_TIMEOUT_S` (default 45 s);
|
||||
the cook stays in the kitchen and, after 3 s idle, parks and drops what they carried.
|
||||
2. The other players see a "Cookie disconnected" label, not a vanished cook.
|
||||
3. Reconnecting with the same token + code mid-round restores the seat and resumes
|
||||
snapshots; time-to-playable ≤ 3 s after page load.
|
||||
4. The client shows a "reconnecting…" overlay with exponential backoff (0.5 s → 8 s cap,
|
||||
jittered) and never spams the server.
|
||||
5. Reconnecting after the grace window releases the seat and the player re-enters as a
|
||||
spectator (or joins the lobby if one exists).
|
||||
6. The server never leaves a ghost connection: a second socket for a token evicts the
|
||||
first, and closed rooms reject stale tokens with a clean error.
|
||||
7. A room whose last seat is gone is closed and its resources freed within one GC cycle.
|
||||
8. A mid-round server-side room exception aborts only that room and tells its players.
|
||||
|
||||
## E2E suite
|
||||
`tests/e2e/test_us09_disconnect_rejoin.py` — kill the socket (context.close of page B /
|
||||
server-side `kill_connection` test hook), assert ghost label, reload and rejoin mid-round,
|
||||
assert playable ≤ 3 s and identical score. Integration: seat grace, GC, stale-token paths.
|
||||
|
||||
## Phases
|
||||
`06_realtime_websocket_protocol`, `08_client_prediction_and_touch`,
|
||||
`12_reconnect_resilience_and_scale`
|
||||
@@ -0,0 +1,30 @@
|
||||
# US-10 — Keep playing after the campaign: endless mode
|
||||
|
||||
**As a** group that finished (or bombed) the campaign,
|
||||
**I want an** endless mode whose difficulty keeps rising and a session scoreboard,
|
||||
**so that** the night keeps going without re-running level 1.
|
||||
|
||||
## Acceptance criteria
|
||||
1. The lobby offers *Endless* as a selectable mode; it starts immediately from a layout
|
||||
drawn from the authored campaign layouts.
|
||||
2. Difficulty scales with completed rounds: order pressure, recipe complexity, time
|
||||
budget, station scarcity, and hazard density all increase monotonically per round
|
||||
(asserted by a property test on the generator).
|
||||
3. Each endless round is announced ("Round 4 — pressure 1.6×") and the failure of a
|
||||
round ends the endless run (score = cumulative).
|
||||
4. The session scoreboard shows, per session token: levels with best score + stars +
|
||||
date, best endless run, total rounds, and total dishes served.
|
||||
5. The scoreboard is visible in the lobby and on the result screen; it is explicitly
|
||||
labelled as this-server-only (in memory, cleared on restart).
|
||||
6. The endless generator only emits validated levels — it runs the same validator as
|
||||
authored content before a round starts, and falls back to the previous layout if a
|
||||
generated candidate fails.
|
||||
7. Scoreboard data never contains personal data beyond the display name the player typed.
|
||||
|
||||
## E2E suite
|
||||
`tests/e2e/test_us10_endless_scoreboard.py` — start endless, finish a round, assert
|
||||
round counter and rising pressure in the snapshot, assert scoreboard rows in the lobby.
|
||||
Plus `tests/unit/test_endless_generator.py` property tests.
|
||||
|
||||
## Phases
|
||||
`13_endless_mode_and_scoreboard`
|
||||
@@ -0,0 +1,30 @@
|
||||
# US-11 — Play on a phone
|
||||
|
||||
**As a** player with only a phone,
|
||||
**I want to** join with a code and control a cook with touch,
|
||||
**so that** the game works on the couch without a keyboard.
|
||||
|
||||
## Acceptance criteria
|
||||
1. The layout works from 360 px wide to 4K: the kitchen scales to fit, the HUD stays
|
||||
legible, nothing overlaps, and there is no page scroll during play.
|
||||
2. Touch controls appear on touch devices: a left thumb-zone drag stick (8-way
|
||||
quantized) and a large right *act* button (hold for progressive work).
|
||||
3. Controls are usable one-handed per thumb, ≥ 44 px hit targets, and do not cover the
|
||||
serve window or the order queue.
|
||||
4. `touch-action: none` on the play surface: no pinch-zoom, no scroll, no double-tap
|
||||
zoom; the on-screen keyboard never appears during play.
|
||||
5. Orientation: portrait plays with a rotated hint (both orientations are playable).
|
||||
6. Touch input uses Pointer Events; the same code path serves mouse (desktop click-to-act
|
||||
is not required — keyboard is the desktop input).
|
||||
7. Lobby joining on a phone (type code, tap create/join) completes in < 20 s and shows
|
||||
the correct focus/type UI (`inputmode="text"`, `autocapitalize="characters"`).
|
||||
8. Framerate on a mid-range mobile browser stays ≥ 30 fps with 4 cooks.
|
||||
|
||||
## E2E suite
|
||||
`tests/e2e/test_us11_touch_mobile.py` — mobile viewport + `has_touch` context: create,
|
||||
join by typed code, move via pointer drag, complete an action, assert no scroll and
|
||||
control hit-target sizes.
|
||||
|
||||
## Phases
|
||||
`07_canvas_client_and_hud`, `08_client_prediction_and_touch`,
|
||||
`14_accessibility_polish_and_hardening`
|
||||
@@ -0,0 +1,29 @@
|
||||
# US-12 — Play accessibly
|
||||
|
||||
**As a** player with low vision, motor limitations, or motion sensitivity,
|
||||
**I want** readable contrast, keyboard-only lobby use, and motion controls,
|
||||
**so that** I can play and follow the game state.
|
||||
|
||||
## Acceptance criteria
|
||||
1. All non-canvas UI (lobby, level list, result, HUD text) is real HTML with a logical
|
||||
focus order, visible focus rings, and ≥ 4.5:1 text contrast (3:1 for large text).
|
||||
2. The entire lobby → level-select → start → result loop is operable with the keyboard
|
||||
only (no pointer required), with sensible default buttons.
|
||||
3. Round start, score changes worth ≥ 10 points, order missed, and the round result are
|
||||
announced through a polite `aria-live` region; per-frame data is never announced.
|
||||
4. `prefers-reduced-motion: reduce` removes screen shake, flicker, and parallax, keeping
|
||||
static hazard markers; the setting is also togglable in-game and persisted locally.
|
||||
5. No information is conveyed by color alone: cooks, orders, and hazards have shape,
|
||||
label, or icon differences; a colorblind-safe palette option is provided.
|
||||
6. Canvas has a text alternative: `role="img"` + an `aria-label` summarizing state, and
|
||||
a DOM "state panel" (toggleable) listing score, timer, orders, and hazards.
|
||||
7. Text scales to 200% without loss of function; hit targets stay ≥ 44 px.
|
||||
8. Sound is optional, off by default, and never required to know the game state.
|
||||
|
||||
## E2E suite
|
||||
`tests/e2e/test_us12_accessibility.py` — keyboard-only loop, focus order, contrast audit
|
||||
of computed styles, `aria-live` announcements for result, reduced-motion class applied.
|
||||
|
||||
## Phases
|
||||
`07_canvas_client_and_hud`, `11_campaign_levels_and_progression`,
|
||||
`14_accessibility_polish_and_hardening`
|
||||
Reference in New Issue
Block a user