phase: 03_simulation_movement_and_interaction
**Final verification pass — phase 03 `simulation_movement_and_interaction` (all 5 task files already in `complete/`)** - Verified all phase-03 deliverables exist and work: `sim/` kernel (`step`, `state_build`, `rng`, `movement`, `collision`, `interact`, `carry`, `replay`), replay fixtures + 3 committed goldens, `docs/SIMULATION.md`. - `bash .agents/validate.sh` → **passed**: ruff `All checks passed!`, pyright `0 errors`, `undercooked-levels validate` green, full `pytest` **1033 passed** (coverage **99.71%** ≥ 90% floor), slow sim stage green, repo-hygiene clean. - Perf smoke (4 players, 2 000 ticks): mean **0.049 ms/tick, 20 423 ticks/s** — ≥ 10 000 ticks/s criterion met (test lives in `test_perf_smoke.py`, which the gate's slow stage runs; the overview names it `test_replay.py`). - Completion criteria: import `undercooked.sim.step.step` ✅ · `tests/sim` green incl. determinism + goldens ✅ · perf ✅ · constant-change-fails-by-design ✅ (committed-hash equality + mutation test naming first bad tick/field; goldens never rewritten by tests) · `docs/SIMULATION.md` ✅ (constants, 8-step order, purity/determinism, re-bless) · full suite + coverage ✅ · app launches: `/healthz` 200, `/` 200, `/api/levels` serves content ✅ · replay CLI `check walk_solo.json` exit 0 ✅ · no regression in phases 01/02 (their suites green) ✅. - Documentation: README.md — fixed one stale inconsistency (Status opening listed only 01/02 complete with 03 pending; now lists 01–03 complete, next pending `04_kitchen_pipeline_and_scoring`); all other README claims (CLI flags, config table, shell text, replay CLI exit codes) verified against code. AGENTS.md — verified consistent with the project as it stands, no changes needed. - No deviations. Next pending phase: **`04_kitchen_pipeline_and_scoring`**.
This commit is contained in:
@@ -10,9 +10,11 @@ email, no password: you create a room, share the 5-character code, play.
|
||||
|
||||
## Status
|
||||
|
||||
Phases **01** (runnable service scaffold) and **02** (level content, schema and
|
||||
validation) are **complete**; the next pending phase is
|
||||
`03_simulation_movement_and_interaction`. What runs today:
|
||||
Phases **01** (runnable service scaffold), **02** (level content, schema and
|
||||
validation), and **03** (the deterministic simulation kernel: fixed-step tick,
|
||||
movement and collision, the action button, the hands, the replay goldens) are
|
||||
**complete**; the next pending phase is `04_kitchen_pipeline_and_scoring`. What
|
||||
runs today:
|
||||
|
||||
- **The service.** The Python toolchain is locked (`uv.lock`), configuration is a
|
||||
single typed, validated `Settings` object (`DEBUGPY` support included), and
|
||||
@@ -29,13 +31,69 @@ validation) are **complete**; the next pending phase is
|
||||
`GET /api/levels/{id}` and `GET /api/config` answer from it, and
|
||||
`undercooked-levels` gives authors the same gate from the command line
|
||||
(`docs/LEVELS.md` is the schema reference).
|
||||
- **The tick.** `sim/state_build.py` builds a round from level + seats + seed and
|
||||
`sim/step.py` advances it in whole `0.05` s substeps (20 Hz, A4) with a splitmix64 RNG
|
||||
seeded from the round seed — pure (no clock, no `random`, nothing mutated), replayable
|
||||
from a seed plus an input script, and 100 % covered. The clock, the input contract and
|
||||
the round's end condition are real. `docs/SIMULATION.md`
|
||||
is the contract, `tests/sim/test_rng.py` locks the generator's published vectors.
|
||||
- **Walking the kitchen.** `sim/movement.py` + `sim/collision.py` are stage 3 of the tick:
|
||||
intent is clamped server-side (`move=(50,0)` walks at 3.5 tiles/s, not 175), accelerated
|
||||
at 30 tiles/s² and integrated one axis at a time against an axis-aligned sweep, so a cook
|
||||
stops flush against a wall instead of inside it, slides along it for free, cannot tunnel a
|
||||
1-tile wall even at the coarsest legal `dt`, and cannot clip a diagonal corner. Walls,
|
||||
counters, void, off-map and the solid fixtures (`spawn_shelf`, `chop_board`, `stove`,
|
||||
`prep`) block; `serve`, `sink` and `trash` do not. Cooks soft-push each other apart
|
||||
symmetrically and positionally — a bump never injects velocity — and only a genuine
|
||||
terrain deadlock drops an input, the later-received one, exactly like the kernel's
|
||||
arbitration. Stun (`0.6` s) pins velocity and position and reports one `stunned` event on
|
||||
entry; facing follows the last non-zero intent, so an action press while standing still
|
||||
does not spin the cook. Proven by hand-computed geometry in `tests/sim/test_movement.py`,
|
||||
by a 2000-substep four-cook random walk from the sim's own RNG that re-checks containment,
|
||||
non-overlap and hash equality after *every* substep
|
||||
(`tests/sim/test_movement_stress.py`), and by the same walk on the shipped levels through
|
||||
the whole `step()` (`tests/sim/test_movement_kernel.py`).
|
||||
- **The action button.** `sim/interact.py` is the second half of stage 2 of the tick:
|
||||
the one `action` flag per frame becomes one press per rising edge, so however long the
|
||||
button is held there is one effect — and one named refusal when there is nothing to do
|
||||
(`DENIED` with `out_of_reach`, `hands_full`, `station_busy`, `contested_lost`, … in the
|
||||
payload, which the client turns into a nudge; a silent no-op is the failure mode PLAN §5
|
||||
bans by name). A press resolves a reach zone — the tile in front, then the two
|
||||
diagonal-front tiles (a 0.6-tile forgiveness beyond the 1-tile reach), then the tile
|
||||
underfoot — and a ladder picks the verb: a full hand sets its item down on the fixture
|
||||
(the spawn shelf is the escape hatch), a free hand picks from a ready fixture or shelf
|
||||
stock, a stunned cook is refused before the target is even read, and a fixture owns
|
||||
what is on it (an onion on a chop board is *to be chopped*, not grabbable). Two cooks
|
||||
pressing the same item in the same substep are arbitrated by arrival, `seq`, then seat
|
||||
index — the loser is told `contested_lost`, never left wondering. The work verbs (chop,
|
||||
cook, plate, serve, wash) are named by the ladder but no-ops until phase 04's pipeline
|
||||
spends them (the long-press trash is real — see "The hands" below); `docs/SIMULATION.md`
|
||||
§"The action button" is the contract. Proven by
|
||||
`tests/sim/test_targeting.py` (hand-computed reach geometry, the press edge through the
|
||||
whole `step()`) and `tests/sim/test_verbs.py` (a table that covers every `Verb` and every
|
||||
`DeniedReason`, plus the gate's contested arbitration).
|
||||
- **The hands.** `sim/carry.py` is what the verbs *do*: one carried item per cook (a
|
||||
second pick is `hands_full`), no floor drops in v1 (a press at open floor is the named
|
||||
`nothing_here`), and a set-down is a *placed object* — `RELEASE` / `PLACE_ON_STATION` /
|
||||
`PLATE_DISH` put the item on the fixture and stamp it with a mark, so a teammate's pick
|
||||
of a set-down within 20 ticks is a `HANDED_OFF` with both ids (the sanctioned pass) and
|
||||
an unmarked pipeline input stays the work press's. The trash is long-press and always
|
||||
available: holding the button over the can for 8 substeps destroys the carried item
|
||||
(`TRASHED`, the raw `−2` score nudge in the payload for phase 04's clamped scoring,
|
||||
`counters["trashed"]` up by one); a short press and a walk-away do nothing. A 100-tick
|
||||
fuzz of random simultaneous presses re-checks the conservation law
|
||||
(`hands + stations + trashed == initial`) after every tick, and a scripted two-player
|
||||
pass-and-contest reaches its committed golden hash. `docs/SIMULATION.md` §"The hand" is
|
||||
the contract (phase 09's co-op E2E asserts exactly this tie-break). Proven by
|
||||
`tests/sim/test_carry.py` and `tests/sim/test_replay_carry.py`.
|
||||
- **The rules are tests, not promises.** The no-external-asset rule is
|
||||
`tests/unit/test_no_external_assets.py`, "no level or recipe id in Python" is
|
||||
`tests/unit/test_content_not_hardcoded.py`, the documentation set is guarded by
|
||||
`tests/unit/test_docs_index.py`, and `bash .agents/validate.sh` is the gate the
|
||||
phase pipeline runs after every task.
|
||||
|
||||
What does not exist yet: the simulation (phases 03-04), rooms and the WebSocket
|
||||
What does not exist yet: the rest of the simulation (the cooking pipeline, order spawning
|
||||
and scoring in phase 04), rooms and the WebSocket
|
||||
protocol (05-06 — the lobby buttons answer "Not implemented until phase 05.") and the
|
||||
playable Canvas renderer (07). Execute phases with the `phased-execution` skill
|
||||
(`auto-phase.sh`); it always takes the lowest-numbered directory still in
|
||||
@@ -117,9 +175,19 @@ uv run pytest # everything, coverage floor included
|
||||
uv run pytest -m "not e2e" # the quick loop: no browser launches
|
||||
uv run playwright install chromium # once, before the first e2e run
|
||||
uv run pytest -m e2e # browser suites only
|
||||
uv run pytest -m slow tests/sim -q --no-cov # the gate's slow stage: goldens + perf smoke
|
||||
bash .agents/validate.sh # the pipeline gate: full suite + ruff + pyright
|
||||
```
|
||||
|
||||
The replay goldens behind `tests/sim/test_replay.py` are checked the same way a
|
||||
level is validated: `uv run python -m undercooked.sim.replay check
|
||||
<tests/fixtures/replays/<name>.json>` runs the committed input script and
|
||||
gates it on `tests/goldens/<name>.golden.json` (exit `0` match, `1` divergence
|
||||
with the first divergent tick and the changed fields, `2` missing script/level/
|
||||
golden). `record` writes a golden — the manual re-bless, legitimate only after a
|
||||
deliberate sim-contract change recorded in the phase task; the contract is
|
||||
[docs/SIMULATION.md](docs/SIMULATION.md) ("Replaying and re-blessing goldens").
|
||||
|
||||
`pytest` with no `-m` runs the browser suites too, so chromium has to be
|
||||
installed for the pipeline gate. Those suites start the app themselves on a free
|
||||
loopback port (`live_server` in `tests/conftest.py`) — nothing needs to be
|
||||
@@ -130,8 +198,11 @@ running first, and no test ever listens on a routable interface.
|
||||
`ruff check .`, `pyright`, `undercooked-levels validate` (unconditional: the same
|
||||
loader and PLAN §8 rules `create_app` runs at startup, so a campaign that cannot
|
||||
validate cannot commit), the **full** `pytest` suite with the 90 % coverage floor,
|
||||
then repo-hygiene checks (nothing tracked that `.gitignore` excludes, no log drops or
|
||||
key material committed). Run it from the repo root or from `.agents/`.
|
||||
the slow sim stage (`pytest -m slow tests/sim`: the replay goldens and the perf
|
||||
smoke, named on their own so a quick loop that skips them cannot let them slip
|
||||
through the gate), then repo-hygiene checks (nothing tracked that `.gitignore`
|
||||
excludes, no log drops or key material committed). Run it from the repo root or
|
||||
from `.agents/`.
|
||||
|
||||
## Container
|
||||
|
||||
@@ -187,9 +258,19 @@ src/undercooked/sim/ pure simulation kernel: entities (`model`), grid geom
|
||||
(`grid`), content schema + loader (`recipe`, `level`,
|
||||
`content` — the YAML contract is docs/LEVELS.md), the
|
||||
level validator (`validation` — PLAN §8 named rules;
|
||||
rule table in docs/LEVELS.md), and the
|
||||
A5 purity guard (`purity`, test-enforced). There is no
|
||||
`step()` yet: the tick arrives with phase 03
|
||||
rule table in docs/LEVELS.md), the fixed-step tick (`step`,
|
||||
`state_build`, `rng`, and the subsystem hooks `movement`
|
||||
(+ `collision`, its pure geometry), `interact` (the action
|
||||
button — reach zone, verb ladder, named denials, contested
|
||||
arbitration, the long-press trash), `carry` (the hands — one
|
||||
item per hand, the marked set-down and counter hand-off,
|
||||
the always-available trash), `stations`, `orders`, `hazards`,
|
||||
`round_state` —
|
||||
contract in docs/SIMULATION.md), and the A5 purity guard
|
||||
(`purity`, test-enforced)
|
||||
src/undercooked/sim/replay.py the replay harness: the input-script format, the golden
|
||||
hashes, `run_script`/`compare`, and the record/check CLI
|
||||
(docs/SIMULATION.md "Replaying and re-blessing goldens")
|
||||
src/undercooked/tools/levels.py the `undercooked-levels` author CLI — validate, list,
|
||||
show [--grid], new, axes; same loader, same rules, no
|
||||
hard-coded content list (docs/LEVELS.md "Authoring workflow")
|
||||
@@ -203,14 +284,17 @@ levels/ data-driven content (YAML): the recipe book (A8) and the
|
||||
app/static/ browser client: the ES-module shell today; Canvas 2D renderer
|
||||
and this repo's SVG art arrive with phase 07
|
||||
tests/ unit/, integration/, e2e/ (one Playwright suite per story) and
|
||||
sim/ (the pure kernel: grid, entities, purity guard, validation);
|
||||
sim/ (the pure kernel: grid, entities, purity guard, validation,
|
||||
the seeded RNG and the tick; plus the replay harness — committed
|
||||
goldens in goldens/, scripts in fixtures/replays/ — and the
|
||||
`slow` perf smoke);
|
||||
fixtures/levels/ is the content dir tests see (a recipe book and no
|
||||
levels; `_bad/` holds the deliberately-broken campaign the `_`
|
||||
discovery rule ignores); conftest.py
|
||||
docs/ DESIGN.md, LEVELS.md (the level & recipe YAML contract),
|
||||
TESTING.md (how this project is verified), DEPLOYMENT.md;
|
||||
PROTOCOL / SIMULATION / CLIENT land with their own phases
|
||||
(the index is PLAN.md §13)
|
||||
SIMULATION.md (the tick contract), TESTING.md (how this project
|
||||
is verified), DEPLOYMENT.md; PROTOCOL / CLIENT land with their
|
||||
own phases (the index is PLAN.md §13)
|
||||
.agents/ PLAN.md, user_stories/, phases/, validate.sh — the phased-execution plan
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user