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,56 @@
|
||||
# Phase 10 — Hazards and pressure mechanics
|
||||
|
||||
**Story:** `US-08_hazards_under_pressure`
|
||||
**Context:** `.agents/PLAN.md` §5 (hazards), §6 (`hazards` level block), `docs/SIMULATION.md`
|
||||
|
||||
## Objective
|
||||
Add the mechanics that create the panic: fire that spreads and is extinguishable, spills
|
||||
that make cooks slip, moving obstacles that stun, and rush waves that shorten the clock —
|
||||
each with a warning window, a counter-play, and an explicit reduced-motion presentation.
|
||||
Every mechanic is authored through the level YAML `hazards` block, never hard-coded per
|
||||
level.
|
||||
|
||||
## Dependencies
|
||||
- `09_co_op_two_player_e2e` (complete) — arbitration, station locks, multi-context harness
|
||||
- `04_kitchen_pipeline_and_scoring` (complete) — order/score/spoil systems hazards plug into
|
||||
- `07_canvas_client_and_hud` (complete) — event→FX pipeline, reduced-motion mode
|
||||
|
||||
## Tasks
|
||||
1. `01_fire.md` — ignition, spread rules, extinguishing (sink/trash), damage to items and orders
|
||||
2. `02_spills_and_slipping.md` — spill creation, slip stun, recovery, cleaning loop
|
||||
3. `03_moving_obstacles.md` — path-driven obstacles, telegraph, stun, item knock-off
|
||||
4. `04_rush_waves_and_pressure_modifiers.md` — timed waves, shortened order timers, spawn-rate ramps, level-declared modifier table
|
||||
5. `05_hazard_presentation_and_warnings.md` — telegraphs, FX, audio-free cues, reduced-motion alternates, onboarding tooltip for the first hazard
|
||||
6. `06_hazard_determinism_and_validator_hardening.md` — determinism under hazard fuzz, validator rules for the new hazard fields, `docs/SIMULATION.md` rules
|
||||
|
||||
## Testing & Quality
|
||||
- Unit (sim): every hazard rule with a fake clock; spread/stun/wave boundaries; interaction
|
||||
matrix (fire × carried item, spill × soft push, obstacle × parked disconnected cook).
|
||||
- Sim determinism: 3 000-tick hazard-heavy replay reproduces hash exactly; a hazard seeded
|
||||
run is bit-identical across two `Room` instances.
|
||||
- E2E (`test_us08_hazards_under_pressure.py`): fire visible + extinguishable by a player,
|
||||
slip visibly stuns and is recoverable, obstacle telegraph appears ≥ its warning window
|
||||
before contact, rush wave visibly shortens the timer, and reduced-motion mode still shows
|
||||
every hazard as a static marker.
|
||||
- Safety floor: hazards never kill, never permanently block the serve window, and never make
|
||||
a level unwinnable (asserted by the phase-11 solver reuse here on the hazard levels).
|
||||
- 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 update `docs/SIMULATION.md` (hazard rules), `docs/LEVELS.md` (every hazard field and
|
||||
its defaults), `docs/CLIENT.md` (warning/FX and reduced-motion mapping), `README.md`
|
||||
(tips: extinguish, mop, watch the telegraph).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/sim -q -k "fire or spill or obstacle or rush"` → green
|
||||
- [ ] `uv run pytest tests/e2e/test_us08_hazards_under_pressure.py -q` → green (all mechanics, both motion modes)
|
||||
- [ ] 3 000-tick hazard-heavy replay is hash-identical to the golden and across two room instances
|
||||
- [ ] every hazard field in `levels/*.yaml` is documented in `docs/LEVELS.md` and rejected by the validator when out of range (schema test)
|
||||
- [ ] no level becomes unwinnable with its hazards enabled (solver bound re-run)
|
||||
- [ ] 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,53 @@
|
||||
# Task 01 — Fire
|
||||
|
||||
**Phase:** `10_hazards_and_escalation_mechanics` · **Story:** `US-08_hazards_under_pressure`
|
||||
|
||||
## Objective
|
||||
Fire as a fair, readable threat: it starts from burnt food, spreads to adjacent
|
||||
combustible tiles on a schedule, damages what is on it, and is put out with water.
|
||||
|
||||
## Work
|
||||
1. `src/undercooked/sim/hazards/fire.py`:
|
||||
- `IgnitionSource`: a `BURNT` item on a `stove`/`chop`/`prep` tile ignites that tile with
|
||||
probability 1 (deterministic, no RNG — reproducibility beats realism here);
|
||||
- `HazardState {kind=FIRE, cell, stage, ticks_in_stage, fuel}`; stages
|
||||
`SPARKING` (warning, `spark_ticks` from level YAML, default 30) → `BURNING` →
|
||||
`OUT`/`EXTINGUISHED`;
|
||||
- spread: while `BURNING`, every `spread_ticks` (level field, default 40) ignite one
|
||||
adjacent `COMBUSTIBLE` tile (counters, not walls/void) in a deterministic order
|
||||
(N, E, S, W) — cap the simultaneous fire count per level (`max_fires`, default 4) so a
|
||||
kitchen can't cascade into an unwinnable state;
|
||||
- effects: items on a `BURNING` tile are destroyed (`WASTED` counter, `ITEM_BURNED`
|
||||
event); a cook entering takes a `STUNNED(short)` and drops the carried item onto the
|
||||
nearest safe tile (never deleted); orders are never destroyed by fire directly — the
|
||||
player loses the ingredients, not the order;
|
||||
- extinguish: interacting with a `sink` (or `trash`) while `action_held` and adjacent to
|
||||
the fire cell puts it out (`FIRE_OUT` event), costs one `dirty` dish if the cook is
|
||||
holding nothing (the bucket is dirty — keeps the sink loop meaningful); water has a
|
||||
cooldown (`water_cooldown_ticks`, default 10) so it cannot be spam-toggled;
|
||||
- disconnected/parked cooks never spread or extinguish fire.
|
||||
2. Level fields (`level.py` + validator + `docs/LEVELS.md`): `hazards.fire.{enabled,
|
||||
spark_ticks, spread_ticks, max_fires, water_cooldown_ticks}` with bounds, and the
|
||||
validator rule: if `fire.enabled`, the level must have ≥ 1 `sink` reachable from every
|
||||
fire-capable tile (BFS) — no fire without a way to fight it.
|
||||
3. Events: `FIRE_IGNITED`, `FIRE_SPREAD`, `FIRE_OUT`, `ITEM_BURNED` (+ client FX mapping in
|
||||
`render/fx.js`, with static markers under reduced motion).
|
||||
4. `tests/sim/test_fire.py` — ignition from burnt food, stage timing at each boundary,
|
||||
spread order and cap, item destruction + counter, cook stun + item relocation (never
|
||||
lost), extinguish path + cooldown, no spread to walls/void/serve window, parked
|
||||
disconnected cook is inert, 3 000-tick determinism with fire enabled.
|
||||
5. `tests/unit/test_level_validator_fire.py` — the "fire requires a reachable sink" rule
|
||||
(positive + two negatives), out-of-range field rejection, defaults filled.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit (sim) + validator tests; boundaries at `spark_ticks-1/spark_ticks/+1`.
|
||||
- Coverage: **>90%** on `sim/hazards/fire.py` (target 95%).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/sim/test_fire.py tests/unit/test_level_validator_fire.py -q` → green
|
||||
- [ ] fire never exceeds `max_fires` and never spreads to non-combustible tiles (asserted)
|
||||
- [ ] a carried item displaced by fire always lands on a reachable tile or is counted in `lost_items` (asserted, never silently dropped)
|
||||
- [ ] `uv run undercooked-levels validate` passes shipped levels with fire enabled
|
||||
- [ ] 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,50 @@
|
||||
# Task 02 — Spills and slipping
|
||||
|
||||
**Phase:** `10_hazards_and_escalation_mechanics` · **Story:** `US-08_hazards_under_pressure`
|
||||
|
||||
## Objective
|
||||
Wet floors: dropped/burst contents make a spill, stepping in it slips you, and mopping is a
|
||||
real chore that competes with cooking.
|
||||
|
||||
## Work
|
||||
1. `src/undercooked/sim/hazards/spill.py`:
|
||||
- creation: `ITEM_BURNED`, a dropped `pot`/`pan` with contents, an ingredient destroyed by
|
||||
an obstacle, and level-declared `hazards.spills.seed_cells` (used by level 5) each create
|
||||
a `HazardState {kind=SPILL, cell, life_ticks}`; multiple spills do not stack on one cell
|
||||
(life refreshes instead);
|
||||
- slip: a cook entering a spill cell rolls a slip (`slip_chance` expressed deterministically
|
||||
as `every_n_th_entry`, default 1 — a spill always slips you on the level that has them, so
|
||||
the mechanic is legible; per-level tunable) → `STUNNED(slip_stun_ticks, default 20)` plus a
|
||||
short scripted slide in the entry direction until a blocking tile (slide never enters
|
||||
`VOID`, never crosses a wall, capped at `max_slide_cells`, default 2);
|
||||
- slide carries the carried item with the cook (it does not fall off mid-slide) unless the
|
||||
slide ends adjacent to a `BURNING` tile;
|
||||
- cleanup: interacting with the spill while `action_held` cleans it (`SPILL_CLEANED`) and adds
|
||||
one `dirty` dish to the sink pool (mop/bucket realism), or the cook can walk the dirty-dish
|
||||
route — both are viable; cleanup has a `clean_cooldown_ticks` (default 6);
|
||||
- spills decay after `life_ticks` (default 300) so an ignored kitchen does not become
|
||||
permanently unplayable; decay emits `SPILL_GONE`.
|
||||
2. Level fields: `hazards.spills.{enabled, every_n_th_entry, slip_stun_ticks,
|
||||
max_slide_cells, life_ticks, clean_cooldown_ticks, seed_cells[]}`; validator: `seed_cells`
|
||||
must be `FLOOR` and reachable from ≥ 2 spawn points (a spill you can never clean is a bug).
|
||||
3. Events `SPILL_CREATED`, `SLIPPED`, `SPILL_CLEANED`, `SPILL_GONE` + FX (decal, slide dust,
|
||||
static marker under reduced motion).
|
||||
4. `tests/sim/test_spills.py` — creation sources, no stacking, slip + slide boundaries
|
||||
(`max_slide_cells`, wall stop, void stop), carried item stays through the slide, cleanup +
|
||||
cooldown + dirty-dish accounting, decay, deterministic entry counting, 3 000-tick replay
|
||||
with spills seeded.
|
||||
5. `tests/unit/test_level_validator_spills.py` — `seed_cells` must be floor and reachable;
|
||||
bounds rejection.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit (sim) + validator; slide math tested with a wall and void fixture map.
|
||||
- Coverage: **>90%**.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/sim/test_spills.py tests/unit/test_level_validator_spills.py -q` → green
|
||||
- [ ] a slide never enters a wall or void and never exceeds `max_slide_cells` (asserted on a fixture map)
|
||||
- [ ] cleanup always adds exactly one dirty dish and respects the cooldown (asserted)
|
||||
- [ ] an ignored spill decays and the round stays winnable (asserted)
|
||||
- [ ] 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,55 @@
|
||||
# Task 03 — Moving obstacles
|
||||
|
||||
**Phase:** `10_hazards_and_escalation_mechanics` · **Story:** `US-08_hazards_under_pressure`
|
||||
|
||||
## Objective
|
||||
Things that move through the kitchen on rails: a telegraph, a stun on contact, and a clear
|
||||
way to avoid them.
|
||||
|
||||
## Work
|
||||
1. `src/undercooked/sim/hazards/obstacle.py`:
|
||||
- authored as `hazards.obstacles[]: {id, path: [[x,y],...], speed_cells_s, mode:
|
||||
pingpong|loop, period_s?, offset_s?, stun_ticks}`; position is a pure function of the
|
||||
tick (`pos_at(tick)`) — no integration drift, trivially replayable and testable;
|
||||
- telegraph: `TELEGRAPH_TICKS` (default 12) before entering a cell, that cell is flagged
|
||||
in the snapshot (`hazard_cells` with intensity) so the client can draw the warning tile
|
||||
before the obstacle is there;
|
||||
- contact: a cook in the obstacle's cell (or the telegraph cell at contact time) takes
|
||||
`STUNNED(stun_ticks)` and drops the carried item one tile back along their movement
|
||||
vector (onto a reachable tile, else `lost_items` + `ITEM_LOST`); the obstacle never
|
||||
moves a cook through a wall;
|
||||
- items on the path are knocked, not destroyed (destroy only if the destination is
|
||||
`BURNING`, reusing the fire rule);
|
||||
- orders and stations are unaffected; an obstacle never occupies the serve-window tile or
|
||||
a spawn tile while a cook is spawning;
|
||||
- two obstacles on intersecting paths pass through each other (documented, avoids
|
||||
deadlock); obstacle vs. parked disconnected cook = stun on the ghost, no effect on state
|
||||
beyond the counter.
|
||||
2. Snapshot/protocol: `hazard_cells` array (`{x, y, kind, intensity}`) added to the state
|
||||
encoding — a wire change, so bump `PROTOCOL_VERSION`, update `docs/PROTOCOL.md`,
|
||||
`tests/unit/test_protocol_doc_sync.py`, and the client renderer's hazard layer.
|
||||
3. Level fields + validator: `path` cells in bounds and traversable (floor, not wall/void),
|
||||
`speed_cells_s` within `0.5..4`, at least one cell on the path reachable by no cook at
|
||||
spawn, obstacles cannot fully separate the serve window from the spawns at any tick
|
||||
(BFS over the union of blocked cells across one full period) — this is the rule that keeps
|
||||
levels fair and is the interesting part of the validator.
|
||||
4. `tests/sim/test_obstacles.py` — `pos_at` periodicity/ping-pong correctness, telegraph
|
||||
timing, stun + knockback placement, no wall traversal, path never disconnects the kitchen
|
||||
(property check over one period), two obstacles passing, determinism across two rooms.
|
||||
5. `tests/unit/test_level_validator_obstacles.py` — bad path, bad speed, unreachable-path,
|
||||
kitchen-severing path (each rejected with the specific message).
|
||||
6. `tests/e2e/test_us08_obstacle_telegraph.py` — the warning tile is visible ≥ telegraph
|
||||
window before contact, contact stuns visibly, reduced motion still shows the marker.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit (sim) + validator + one e2e; the path/BFS checks are property-tested over a full period.
|
||||
- Coverage: **>90%** on `obstacle.py` and the new validator rules.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/sim/test_obstacles.py tests/unit/test_level_validator_obstacles.py -q` → green
|
||||
- [ ] no obstacle path ever disconnects the serve window from the spawns (asserted over a full period)
|
||||
- [ ] the telegraph precedes contact by exactly `TELEGRAPH_TICKS` (asserted) and the e2e sees it
|
||||
- [ ] `PROTOCOL_VERSION` bump + doc sync test green
|
||||
- [ ] 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
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
# Task 04 — Rush waves and pressure modifiers
|
||||
|
||||
**Phase:** `10_hazards_and_escalation_mechanics` · **Story:** `US-08_hazards_under_pressure`
|
||||
|
||||
## Objective
|
||||
Escalate inside a single round: timed waves that shorten clocks and raise the spawn rate,
|
||||
declared in level YAML so every level composes pressure from the same vocabulary.
|
||||
|
||||
## Work
|
||||
1. `src/undercooked/sim/hazards/rush.py`:
|
||||
- `hazards.rush.waves[]: {at_s, duration_s?, order_time_mult, spawn_mult?, max_orders_delta?,
|
||||
label?}`; effects applied as multipliers over the level's base order budget, computed in
|
||||
`orders.py` via a `PressureModel` the round step consults (no ad-hoc timer edits);
|
||||
- deterministic by tick: `pressure_at(tick)` is a pure function; overlapping waves compose
|
||||
multiplicatively with a documented clamp (`order_time_mult ≥ 0.35`, `spawn_mult ≤ 3.0`) so
|
||||
a level cannot author an impossible squeeze;
|
||||
- `RUSH_START` / `RUSH_END` events with the `label` for the HUD banner; the HUD shows the
|
||||
active wave name and the modified timer scale;
|
||||
- `hazards.modifiers` (existing level field) wired to the same hook: `slow_cook`,
|
||||
`fast_spoil`, `no_plates_until_wash`, `dark_kitchen` (vision-limited client overlay,
|
||||
server unchanged), each a named, tested rule with a `docs/SIMULATION.md` entry — a
|
||||
modifier that is declared but unimplemented fails the validator.
|
||||
2. Validator + `docs/LEVELS.md`: wave bounds, monotonic `at_s`, clamp documentation, and a
|
||||
rule that a level's total `order_time_mult` schedule cannot drop below the solver's minimum
|
||||
feasible window (reuses the phase-11 solver hook if present; otherwise the static bound
|
||||
`time_limit × min(order_time_mult) ≥ shortest_recipe_seconds × 2`).
|
||||
3. `tests/sim/test_rush_waves.py` — `pressure_at` step boundaries at `at_s`, `at_s+durations`,
|
||||
clamp behaviour, overlap composition, event emission once per wave, no wave mutating
|
||||
already-spawned orders (documented: pressure affects new orders only), determinism.
|
||||
4. `tests/sim/test_modifiers.py` — one test per modifier, plus "declared-but-unimplemented
|
||||
fails".
|
||||
5. `tests/e2e/test_us08_rush_wave.py` — HUD banner appears at the wave, the order time bar
|
||||
visibly shorter for a post-wave order, and the round remains winnable.
|
||||
6. `docs/SIMULATION.md` — pressure model as a formula, and the "pressure never retroactively
|
||||
shortens a live order" rule.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit (sim) + validator + e2e; boundary tests at every `at_s`.
|
||||
- Coverage: **>90%**.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/sim/test_rush_waves.py tests/sim/test_modifiers.py -q` → green
|
||||
- [ ] pressure affects only orders spawned after the wave (asserted)
|
||||
- [ ] clamps hold for a deliberately over-aggressive test level (asserted, not clamped away silently)
|
||||
- [ ] declaring an unknown/unimplemented modifier fails `uv run undercooked-levels validate`
|
||||
- [ ] `uv run pytest tests/e2e/test_us08_rush_wave.py -q` → green
|
||||
- [ ] 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
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
# Task 05 — Hazard presentation, warnings, and reduced motion
|
||||
|
||||
**Phase:** `10_hazards_and_escalation_mechanics` · **Story:** `US-08_hazards_under_pressure`, `US-12_play_accessibly`
|
||||
|
||||
## Objective
|
||||
Make every hazard readable at a glance on any screen and without motion — a player must be
|
||||
able to survive hazards they cannot animate.
|
||||
|
||||
## Work
|
||||
1. `app/static/js/render/hazards.js` — draw from `hazard_cells`: intensity ramp, telegraph
|
||||
(dashed outline + 3-pip countdown), fire stages, spill decal, obstacle body; obstacles
|
||||
drawn on the player layer so they are never hidden behind a counter.
|
||||
2. Non-color, non-motion redundancy (required, not optional):
|
||||
- each hazard kind has a distinct glyph (flame / droplet / arrow) drawn at the cell,
|
||||
- a textual status strip in the HUD (`FIRE ×2`, `WET FLOOR`, `CONVEYOR`, `RUSH: ORDER RUSH`)
|
||||
with `aria-live="polite"` on appearance only (not every tick),
|
||||
- `data-motion="reduced"`: no pulsing, no screen shake, no particle storm — static markers,
|
||||
full-opacity glyphs, telegraph shown as a filled tile instead of an animation,
|
||||
- colorblind palette keeps hazards distinguishable (guard: hazard glyph set must be drawn
|
||||
even when colors are forced to grayscale in the test).
|
||||
3. First-hazard onboarding: the first time a level's hazard appears, a one-time caption
|
||||
("Fire! Use the sink to put it out") for 3 s, dismissible, suppressed for repeats and
|
||||
stored per session in `sessionStorage` — no tutorial screens.
|
||||
4. `assets_src/fx.svg` extensions (flame stages, droplet, arrow, telegraph pips) regenerated
|
||||
into `index.json` by `tools/gen_art_index.py` (extend `EXPECTED_IDS`).
|
||||
5. `tests/e2e/test_us08_hazard_presentation.py`:
|
||||
- telegraph pixel region changes before contact (variance sample, not screenshot equality),
|
||||
- HUD status strip lists the active hazard kinds within 200 ms,
|
||||
- reduced-motion context: no CSS `transform: translate` shake on the canvas wrapper, glyphs
|
||||
still present (probe: draw the reduced-motion frame and assert non-background pixels at
|
||||
the hazard cell),
|
||||
- caption appears once and not again after a retry,
|
||||
- grayscale filter context: hazard cells still differ from non-hazard cells by luminance.
|
||||
6. `docs/CLIENT.md` + `docs/SIMULATION.md` cross-reference: the redundancy contract (glyph +
|
||||
text + color, never color alone).
|
||||
|
||||
## Testing & Quality
|
||||
- E2E with four contexts (normal, reduced motion, forced grayscale, colorblind palette).
|
||||
- Coverage: **>90%** on any server-side change (none expected beyond event payloads).
|
||||
- Full suite green.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/e2e/test_us08_hazard_presentation.py -q` → green across all four contexts
|
||||
- [ ] every hazard kind has a glyph, a HUD text label, and a distinct color (a test walks the hazard enum against the mapping)
|
||||
- [ ] reduced motion removes shake/pulses but keeps every hazard visible (asserted by pixel probe)
|
||||
- [ ] the first-hazard caption shows once per session and is dismissible
|
||||
- [ ] 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
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
# Task 06 — Hazard determinism, fuzz, and validator hardening
|
||||
|
||||
**Phase:** `10_hazards_and_escalation_mechanics` · **Story:** `US-08_hazards_under_pressure`
|
||||
|
||||
## Objective
|
||||
Prove the hazard suite cannot desync, cannot wedge a round, and cannot be authored into an
|
||||
unwinnable level.
|
||||
|
||||
## Work
|
||||
1. `tests/sim/test_hazard_determinism.py`:
|
||||
- property-based (hypothesis-free, seeded random generators are enough) fuzz: 200 generated
|
||||
hazard configurations (random fire/spill/obstacle/rush combos on the shipped maps) ×
|
||||
600 ticks with 4 scripted bots; assert two runs of the same config+seed produce identical
|
||||
state hashes and identical event logs;
|
||||
- no-crash sweep: NaN/inf checks on every numeric field, `stun` never negative, hazard
|
||||
counts never exceed caps, no exception escapes `step()`;
|
||||
- invariants every run: serve window reachable, ≥ 1 spawn tile free, item count + wasted +
|
||||
lost conserved, dirty-dish pool ≤ plates total.
|
||||
2. `tests/sim/test_hazard_fairness.py` — for each hazard level, the solver bound re-runs with
|
||||
hazards enabled: the level is still finishable within `time_limit` and the star thresholds
|
||||
stay achievable (assert the same invariant phase 11 establishes; if the solver hook does not
|
||||
exist yet, use a scripted greedy bot that must reach `target_orders`).
|
||||
3. Validator extension (`level.py`) + tests: cross-field rules that only make sense once all
|
||||
hazards exist — fire enabled but no sink (phase 10 task 01) plus: spill seeds plus obstacles
|
||||
may not block the only path to the sink; `max_fires` ≤ combustible tiles; a level with ≥ 2
|
||||
hazard kinds must have `time_limit ≥ 100` (pressure needs room); `rush` waves must not start
|
||||
before `t=20 s`.
|
||||
4. `docs/SIMULATION.md` — "Hazard invariants" section listing exactly the properties the fuzz
|
||||
asserts (this is the contract future phases must not break).
|
||||
5. Replay golden: commit `tests/sim/goldens/hazards_3000s.json` (config, seed, hash chain
|
||||
sample, final hash) produced by a documented `uv run python tools/record_hazard_golden.py`.
|
||||
|
||||
## Testing & Quality
|
||||
- Fuzz + invariant + golden; the suite must run in < 90 s (mark `slow` if it exceeds that, and
|
||||
still run it in `validate.sh`).
|
||||
- Coverage: **>90%** on new/modified code; hazard modules ≥ 95%.
|
||||
- Full suite green.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/sim/test_hazard_determinism.py tests/sim/test_hazard_fairness.py -q` → green
|
||||
- [ ] `uv run python tools/record_hazard_golden.py` regenerates the golden byte-identically (run twice, compare hashes)
|
||||
- [ ] corrupting one generated hazard config in a scratch copy makes a validator rule fire (demonstrated, then reverted)
|
||||
- [ ] every invariant in `docs/SIMULATION.md` names the test that enforces it
|
||||
- [ ] 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