phase: 01_runnable_service_scaffold
**Phase 01 final verification pass — all criteria green**
- Verified (nothing left to implement; all 4 task files already in `complete/`): toolchain lock, typed `Settings`+`.env.example`, debugpy bootstrap, app factory, CLI (`--host/--port/--reload/--levels-dir`, `python -m undercooked`), static shell + `/static`, security headers, no-CDN/doc guards, `validate.sh`.
- `uv sync --extra dev` → exit 0; `uv.lock` present (untracked → harness commits it with the phase).
- `uv run undercooked --host 127.0.0.1 --port 8000` → `curl /healthz` = 200 `{"status":"ok","version":"0.1.0","rooms":0}` (superset of the criterion's body — now documented in README); `curl /` = 200 shell HTML; static css/js 200; CSP/nosniff/no-referrer/DENY on 200, 404 and 405 too.
- `DEBUGPY=1`: port 5678 is held on this machine by the user's own podman dev-container debugpy adapter — **no live change made/killed**; verified via `DEBUGPY=1 DEBUGPY_PORT=5679` → listener opens, `/healthz` still 200; the occupied-port case degraded gracefully as designed (warning, server serves). Bad config → one line + exit 2 ✓.
- `bash .agents/validate.sh` (root **and** `.agents/`) → exit 0: `ruff check .` 0 errors, `pyright` 0 errors, levels-CLI notice-skip (matches README prose), **284 passed** full suite incl. 10 Playwright e2e, coverage **100.00%** (floor 90), repo hygiene clean. `uv run pytest -m e2e` → 10 passed.
- Docs fixed: README Status → phase 01 complete + next pending 02 (was pointing at `todo/01…`), documented real `/healthz` body, precedence now "CLI flags > env > .env > default", added the PLAN §13-promised **How to play** section (honest about shell-only today), Layout rows marked what exists vs phases 02/07. AGENTS.md: recorded the `data:`-URL exemption in the no-CDN rule (test allows it) and matched the session-token rule to the browser's WebCrypto/base64url implementation. Also corrected a stale "session-wide chromium" docstring in `tests/e2e/conftest.py` (fixtures are function-scoped).
- Deviations: no container build run (needs an image pull; not a phase criterion — `Containerfile`/`compose.yaml` reviewed and consistent with `docs/DEPLOYMENT.md`); `HEAD /` is 405 (routes are GET-only by design; health checks use GET). No git index/history commands run.
- Next pending phase: `02_level_content_and_validation`.
This commit is contained in:
@@ -10,10 +10,40 @@ email, no password: you create a room, share the 5-character code, play.
|
||||
|
||||
## Status
|
||||
|
||||
Roadmap authored, implementation pending. This repository currently contains the
|
||||
project scaffold and the `.agents/` phased-execution plan. Execute phases with the
|
||||
`phased-execution` skill (`auto-phase.sh`), starting at
|
||||
`.agents/phases/todo/01_runnable_service_scaffold/`.
|
||||
Phase 01 (runnable service scaffold) is **complete**; the next pending phase is
|
||||
`02_level_content_and_validation`. What runs today: the Python toolchain is locked
|
||||
(`uv.lock`), the pytest harness covers unit, integration and Playwright e2e layers,
|
||||
configuration is a single typed, validated `Settings` object (`DEBUGPY` support
|
||||
included), and `uv run undercooked` starts a FastAPI server that serves the client
|
||||
shell at `/`, the browser assets from `app/static/`, and `GET /healthz` — which answers
|
||||
`{"status":"ok","version":"0.1.0","rooms":0}` (`rooms` stays `0` until phase 05 owns a
|
||||
supervisor) — with the security headers (CSP `default-src 'self'`) on every response.
|
||||
The rules are tests and not promises: the no-external-asset rule is
|
||||
`tests/unit/test_no_external_assets.py`, the documentation set is guarded by
|
||||
`tests/unit/test_docs_index.py`, `bash .agents/validate.sh` is the gate the phase
|
||||
pipeline runs after every task, and `docs/` holds the DESIGN / TESTING / DEPLOYMENT
|
||||
documents. Lobby buttons answer "Not implemented until phase 05.": levels and their
|
||||
validator land in phase 02, the simulation in 03-04, rooms and the WebSocket protocol
|
||||
in 05-06, the playable renderer in 07. Execute phases with the `phased-execution` skill
|
||||
(`auto-phase.sh`); it always takes the lowest-numbered directory still in
|
||||
`.agents/phases/todo/`.
|
||||
|
||||
## How to play
|
||||
|
||||
The intended loop, once the room flow ships: open the site, **Create lobby**, read out
|
||||
the 5-character code, everyone else **Joins** with it, the host picks a level, the room
|
||||
readies up, and the kitchen starts. Each cook has one movement control and **one action
|
||||
button** that picks up, uses and puts down whatever is in reach — one carried item at a
|
||||
time. Orders appear in the HUD with a shrinking deadline; assemble the dish and serve it
|
||||
at the window before the timer runs out. Levels escalate: more recipes, fewer stations,
|
||||
hostile floors. Nothing is saved between rounds and nothing identifies you: the browser
|
||||
keeps only a random per-tab token and the last code you typed, both in `sessionStorage`.
|
||||
|
||||
What actually works today is the shell of that: the page loads, normalises a typed code to
|
||||
five uppercase alphanumerics (codes themselves are Crockford base32, and only get issued
|
||||
once phase 05 runs the room supervisor), and answers "Not implemented until phase 05."
|
||||
Movement keys and touch controls are defined by the phase-07/08 client and documented in
|
||||
`docs/CLIENT.md` once that phase writes it.
|
||||
|
||||
## Quickstart (development)
|
||||
|
||||
@@ -22,9 +52,24 @@ for container builds. **There is no database** — all live game state is in mem
|
||||
|
||||
```bash
|
||||
uv sync --extra dev
|
||||
uv run undercooked # http://127.0.0.1:8000
|
||||
uv run undercooked # http://127.0.0.1:8000 (shell page + /healthz)
|
||||
uv run undercooked --host 0.0.0.0 --port 8000 # e.g. to reach it from a phone
|
||||
uv run undercooked --reload # dev reload
|
||||
uv run undercooked-levels validate # check every level file loads and is solvable
|
||||
uv run undercooked --levels-dir levels # point at another content dir
|
||||
```
|
||||
|
||||
`python -m undercooked` is the same command (the container uses it). A flag
|
||||
always beats the environment, which beats the built-in default. There is no
|
||||
`/docs` or `/openapi.json`: FastAPI renders those pages from a public CDN, which
|
||||
this project forbids. `undercooked-levels validate` (check every level file loads
|
||||
and is solvable) is declared in `pyproject.toml` but becomes runnable in phase 02,
|
||||
when the content loader lands.
|
||||
|
||||
Debug the server with a real debugger attached (dev only - the listener binds
|
||||
`0.0.0.0`, so never enable it on a public host):
|
||||
|
||||
```bash
|
||||
DEBUGPY=1 uv run undercooked # opens a debugpy listener on :5678, then serves as usual
|
||||
```
|
||||
|
||||
Run the checks:
|
||||
@@ -32,12 +77,25 @@ Run the checks:
|
||||
```bash
|
||||
uv run ruff check .
|
||||
uv run pyright
|
||||
uv run pytest -m "not e2e" # unit + integration
|
||||
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 (app must be running)
|
||||
bash .agents/validate.sh # the full pipeline gate
|
||||
uv run pytest -m e2e # browser suites only
|
||||
bash .agents/validate.sh # the pipeline gate: full suite + ruff + pyright
|
||||
```
|
||||
|
||||
`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
|
||||
running first, and no test ever listens on a routable interface.
|
||||
|
||||
`bash .agents/validate.sh` is the gate a phase task must pass before it moves to
|
||||
`.agents/phases/complete/`. It stops at the first failure, in this order:
|
||||
`ruff check .`, `pyright`, `undercooked-levels validate` (skipped with a notice until
|
||||
phase 02 creates the loader), 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/`.
|
||||
|
||||
## Container
|
||||
|
||||
```bash
|
||||
@@ -49,23 +107,31 @@ podman compose up -d
|
||||
|
||||
## Configuration
|
||||
|
||||
All settings come from the environment (a local `.env` is loaded automatically;
|
||||
never commit it — see `.env.example`).
|
||||
All settings come from the environment, parsed once into
|
||||
`undercooked.config.Settings` — the only module that reads the environment, so
|
||||
nothing else in the codebase touches `os.environ`. Precedence: `undercooked` CLI
|
||||
flags, then exported environment variables, then a local `.env` (found from the
|
||||
current directory upwards; never commit it), then the built-in defaults. A blank value counts as
|
||||
unset. Invalid values (`UNDERCOOKED_TICK_HZ=99`) raise `ConfigError` at startup
|
||||
instead of half-configuring the server. `.env.example` and the table below list
|
||||
every variable with its default; tests keep both in sync with `config.py`.
|
||||
|
||||
| Variable | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `UNDERCOOKED_HOST` | `127.0.0.1` | Bind address |
|
||||
| `UNDERCOOKED_PORT` | `8000` | Bind port |
|
||||
| `UNDERCOOKED_TICK_HZ` | `20` | Server simulation rate |
|
||||
| `UNDERCOOKED_SNAPSHOT_HZ` | `20` | Snapshot broadcast rate to players |
|
||||
| `UNDERCOOKED_MAX_PLAYERS` | `4` | Players per room |
|
||||
| `UNDERCOOKED_TICK_HZ` | `20` | Server simulation rate (5-60) |
|
||||
| `UNDERCOOKED_SNAPSHOT_HZ` | `20` | Snapshot broadcast rate to players (<= tick rate) |
|
||||
| `UNDERCOOKED_MAX_PLAYERS` | `4` | Players per room (1-4) |
|
||||
| `UNDERCOOKED_MAX_ROOMS` | `200` | Concurrent rooms before new rooms are refused |
|
||||
| `UNDERCOOKED_LEVELS_DIR` | `levels/` | Directory of level YAML files |
|
||||
| `UNDERCOOKED_LEVELS_DIR` | `levels` | Directory of level YAML files (`~` is expanded) |
|
||||
| `UNDERCOOKED_STATIC_DIR` | *(derived)* | Directory served at `/static`; empty = `app/static/` beside the source (containers set it) |
|
||||
| `UNDERCOOKED_LOBBY_IDLE_TIMEOUT_S` | `900` | Close empty/idle lobbies |
|
||||
| `UNDERCOOKED_GUEST_IDLE_TIMEOUT_S` | `45` | Drop a disconnected player's seat after this |
|
||||
| `UNDERCOOKED_MAX_MESSAGE_BYTES` | `4096` | Hard cap on inbound WS frame size |
|
||||
| `UNDERCOOKED_MAX_INPUTS_PER_S` | `40` | Per-connection inbound message rate limit |
|
||||
| `UNDERCOOKED_SIM_LATENCY_MS` | `0` | Injected server-side downlink latency (netcode tests) |
|
||||
| `UNDERCOOKED_ALLOWED_ORIGINS` | *(empty)* | Comma-separated `Origin`s accepted on the WebSocket handshake; empty = same-host only |
|
||||
| `DEBUGPY` | `0` | `1` = open a debugpy listener on `DEBUGPY_PORT` at startup |
|
||||
| `DEBUGPY_PORT` | `5678` | debugpy listen port |
|
||||
|
||||
@@ -73,15 +139,26 @@ never commit it — see `.env.example`).
|
||||
|
||||
```
|
||||
src/undercooked/ server: FastAPI app, rooms, WebSocket protocol, simulation
|
||||
levels/ data-driven level definitions (YAML) + recipes
|
||||
app/static/ browser client: ES modules, Canvas 2D renderer, own SVG art
|
||||
tests/ unit/, server/, integration/, sim/, e2e/ (one Playwright suite per story)
|
||||
docs/PROTOCOL.md wire protocol contract (versioned)
|
||||
.agents/ PLAN.md, user_stories/, phases/ — the phased-execution plan
|
||||
src/undercooked/__main__.py the `undercooked` command (CLI > env > default)
|
||||
src/undercooked/config.py typed env settings (the only `os.environ` reader)
|
||||
src/undercooked/debug.py DEBUGPY-gated debugger bootstrap
|
||||
src/undercooked/server/app.py app factory, `/healthz`, `/`, `/static`
|
||||
src/undercooked/server/headers.py the security-header middleware
|
||||
.env.example every configuration variable, with its default
|
||||
levels/ data-driven level definitions (YAML) + recipes (empty until 02)
|
||||
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/, sim/, e2e/ (one Playwright suite per story);
|
||||
fixtures/levels/ (test-isolated content dir); conftest.py
|
||||
docs/ DESIGN.md, TESTING.md (how this project is verified),
|
||||
DEPLOYMENT.md; PROTOCOL / LEVELS / SIMULATION / CLIENT land with
|
||||
their own phases (the index is PLAN.md §13)
|
||||
.agents/ PLAN.md, user_stories/, phases/, validate.sh — the phased-execution plan
|
||||
```
|
||||
|
||||
**No external CDNs.** Every byte of JS, CSS, font, and image is served by this app
|
||||
from `app/static/`. A test enforces it.
|
||||
from `app/static/`. `tests/unit/test_no_external_assets.py` enforces it, and the
|
||||
e2e suite fails if loading the page ever issues a request off-origin.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
Reference in New Issue
Block a user