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:
@@ -19,7 +19,9 @@
|
||||
order: `ruff check .` → `pyright` → `undercooked-levels validate` (unconditional:
|
||||
the same loader + PLAN §8 rules `create_app` runs at startup, over
|
||||
`$UNDERCOOKED_LEVELS_DIR`) → the **full** `pytest` suite with the coverage floor →
|
||||
repo-hygiene checks. It runs 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) → repo-hygiene checks. It runs from the repo root
|
||||
or from `.agents/`.
|
||||
- New work is authored by the `phase-authoring` skill as a phase directory — not
|
||||
implemented straight from chat.
|
||||
- **Never** modify `.agents/PLAN.md` or anything under `.agents/phases/complete/`.
|
||||
@@ -123,15 +125,37 @@
|
||||
server mints one, `crypto.getRandomValues` + base64url in `app/static/js/app.js`,
|
||||
which is the same entropy and the only option in a browser) held in
|
||||
`sessionStorage`. Names are sanitized, length-capped, non-authoritative.
|
||||
- **Movement is swept, never corrected.** Stage 3 (`sim/movement.py`) integrates one axis
|
||||
at a time and clamps each axis with `sim/collision.sweep_axis()` to the greatest legal
|
||||
delta, so a body is stopped *before* it intrudes — a cook never ends a tick inside a
|
||||
blocked tile, at any `dt` the kernel can hand it, and sliding needs no special case. Two
|
||||
rules belong to `collision.py` alone and must not be duplicated in a caller: which tiles
|
||||
and fixtures block (`PASS_THROUGH_STATIONS` = serve/sink/trash; shelf, chop board, stove
|
||||
and prep block), and which cells a body covers (`body_cells()`, `_edge_cells()`) — the
|
||||
footprint and the sweep have to answer with the same arithmetic or a cook ends a tick
|
||||
1e-16 inside a wall, which is invisible and fatal for a replay golden. Cooks push cooks
|
||||
positionally only (no velocity is ever injected by a push), and only a genuine terrain
|
||||
deadlock drops an input — the later-received one, the same arbitration as
|
||||
`resolve_inputs`. `PlayerInput.move` is untrusted: `intent_direction()` clamps its length,
|
||||
so a client sending `(50, 0)` walks at `MAX_SPEED` like everyone else.
|
||||
- **Frame and rate limits are enforced, not suggested**: `max_message_bytes`,
|
||||
`max_inputs_per_s`, room caps, and message-type whitelists per room state.
|
||||
- **Protocol changes need a version bump**: editing a wire message means editing
|
||||
`src/undercooked/server/protocol.py`, `docs/PROTOCOL.md`, and the client in the same task,
|
||||
and bumping `PROTOCOL_VERSION` (PLAN §11).
|
||||
- **Determinism is a contract.** `sim/step.py` (phase 03) must produce identical state
|
||||
given the same seed and input script — the replay tests in `tests/sim/` are the guard
|
||||
once the tick exists; until then `tests/sim/test_purity.py` holds the A5 boundary that
|
||||
determinism depends on.
|
||||
- **Determinism is a contract.** `sim/step.py` advances the round in whole `STEP_DT`
|
||||
(0.05 s) substeps with `dt` clamped to `[0, MAX_DT]`, a fixed subsystem order, and no
|
||||
randomness outside `sim/rng.py` (splitmix64, seeded by `KitchenState.rng_state`) — so the
|
||||
same seed plus the same input script produces identical state. The guard is
|
||||
`tests/sim/test_rng.py` (published golden vectors) + `tests/sim/test_step_kernel.py`
|
||||
(clamping, accumulation, purity, cross-run hash equality); the committed replay
|
||||
goldens (`tests/goldens/*.golden.json` against `tests/sim/test_replay.py`, the
|
||||
input-script format and the `python -m undercooked.sim.replay record|check` CLI
|
||||
in `src/undercooked/sim/replay.py`) and the perf smoke (`tests/sim/test_perf_smoke.py`,
|
||||
`slow`) close it out. `tests/sim/test_purity.py` holds the A5 boundary the
|
||||
whole thing rests on. The contract — constants, the 8-stage order, what each hook still
|
||||
owes — is `docs/SIMULATION.md`; change the tick, the RNG or the step order and that file
|
||||
and the goldens move in the same commit.
|
||||
- **ntfy is outbound-only.** Never use it to ask questions or wait for approval.
|
||||
If a task is ambiguous, make the best judgment consistent with PLAN.md, or stop
|
||||
and let the user rewrite the task.
|
||||
@@ -148,6 +172,10 @@
|
||||
|---|---|
|
||||
| Configuration (env vars → typed `Settings`), debugpy bootstrap | `src/undercooked/config.py`, `.env.example`, `src/undercooked/debug.py` |
|
||||
| Pure game rules (movement, stations, orders, scoring, hazards) | `src/undercooked/sim/` |
|
||||
| The fixed-step tick: `step`/`build_state`, the seeded RNG, the subsystem hooks (contract: `docs/SIMULATION.md`) | `src/undercooked/sim/step.py`, `state_build.py`, `rng.py`, `movement.py`, `interact.py`, `carry.py`, `stations.py`, `orders.py`, `hazards.py`, `round_state.py` |
|
||||
| Movement/collision: velocity integration, facing, stun, soft push, and the pure geometry under it (what blocks, the swept stop, the body footprint) | `src/undercooked/sim/movement.py`, `src/undercooked/sim/collision.py` (rules in `docs/SIMULATION.md` §"Movement and collision") |
|
||||
| The action button: reach-zone targeting, the verb ladder, named `DENIED` reasons, the one-press-one-event gate, contested arbitration and the long-press trash | `src/undercooked/sim/interact.py` (rules in `docs/SIMULATION.md` §"The action button") |
|
||||
| The hands: one-item-per-hand carry, the marked set-down and the counter hand-off, the always-available long-press trash (contract: `docs/SIMULATION.md` §"The hand") | `src/undercooked/sim/carry.py` |
|
||||
| Level/recipe content | `levels/*.yaml` |
|
||||
| Content schema, ASCII-grid parser, content loader, playability validator (contract: `docs/LEVELS.md`) | `src/undercooked/sim/recipe.py`, `level.py`, `content.py`, `validation.py` |
|
||||
| Rooms, lobby lifecycle, session records | `src/undercooked/server/rooms.py`, `room.py` |
|
||||
@@ -155,6 +183,7 @@
|
||||
| HTTP app (`/`, `/healthz`, `/static`), lifespan, startup content gate | `src/undercooked/server/app.py` |
|
||||
| Content HTTP API (`/api/levels`, `/api/levels/{id}`, `/api/config`) + strict response models | `src/undercooked/server/routes_content.py` |
|
||||
| Author CLI `undercooked-levels` (`validate`, `list`, `show`, `new`, `axes`); workflow in `docs/LEVELS.md` | `src/undercooked/tools/levels.py` |
|
||||
| The replay harness: input scripts, golden hashes, `run_script`/`compare`, the `record`/`check` CLI (contract: `docs/SIMULATION.md` "Replaying and re-blessing goldens") | `src/undercooked/sim/replay.py`, `tests/fixtures/replays/`, `tests/goldens/`, `tests/sim/test_replay.py`, `tests/sim/test_perf_smoke.py` (`slow`) |
|
||||
| Security headers (CSP `default-src 'self'`, no-sniff, frame-ancestors) | `src/undercooked/server/headers.py` |
|
||||
| CLI entry point (`undercooked`): flags → `--help` → serve | `src/undercooked/__main__.py` |
|
||||
| Renderer, HUD, input, interpolation | `app/static/js/`, `app/static/assets/` |
|
||||
@@ -165,8 +194,9 @@
|
||||
Tests mirror that: `tests/unit/` (pure, no-I/O logic incl. HTTP-route unit tests),
|
||||
`tests/integration/` (in-process app boot, live uvicorn + WebSocket clients), `tests/sim/`
|
||||
(the pure kernel without a server: grid geometry, entity model, the A5 purity guard,
|
||||
the level validator — headless determinism and replays join it with the tick in
|
||||
phase 03), `tests/e2e/` (Playwright, named `test_usNN_*.py`, marked `e2e`). `tests/` is
|
||||
the level validator, the seeded RNG's golden vectors and the tick's determinism —
|
||||
plus the replay harness: committed goldens in `tests/goldens/`, scripts in
|
||||
`tests/fixtures/replays/`, and the `slow` perf smoke), `tests/e2e/` (Playwright, named `test_usNN_*.py`, marked `e2e`). `tests/` is
|
||||
not a package; shared fixtures live in `tests/conftest.py`, and the autouse fixture there
|
||||
points `UNDERCOOKED_LEVELS_DIR` at `tests/fixtures/levels/` so no test silently reads the
|
||||
shipped `levels/`.
|
||||
|
||||
Reference in New Issue
Block a user