# Undercooked A cooperative, **in-browser**, Overcooked-style kitchen game: 1–4 players share one kitchen over WebSockets, cook orders against the clock, and climb a campaign of levels that get progressively harder. Lobbies are **anonymous** — no accounts, no email, no password: you create a room, share the 5-character code, play. > Working title. "Overcooked" is a trademark of Ghost Town Games/Studio MDG; this > project is an independent homage with its own art, code, and content. ## Status 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) Requires [`uv`](https://docs.astral.sh/uv/), Python ≥ 3.12, and `podman`/`docker` for container builds. **There is no database** — all live game state is in memory. ```bash uv sync --extra dev 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-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: ```bash uv run ruff check . uv run pyright 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 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 podman build -t undercooked -f Containerfile . podman run --rm -p 8000:8000 undercooked # or podman compose up -d ``` ## Configuration 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 (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 (`~` 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 | ## Layout ``` src/undercooked/ server: FastAPI app, rooms, WebSocket protocol, simulation 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/`. `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 Read [AGENTS.md](AGENTS.md) first: it points at `.agents/PLAN.md` (LOCKED decisions) and the phase protocol. Code changes land by executing phase task files, not by editing ad hoc.