# 1. Architecture overview ## Components | Component | Folder | Language / stack | Runs as | Responsibility | |-----------|--------|------------------|---------|----------------| | Website | `wizard_website/` | React 19, TypeScript, Vite, CSS modules | static files served by nginx (container port 8082) | UI: start page, lobby, game table | | Game server | `wizard_server/app/` | Python 3.13, FastAPI, uvicorn | one process (container port 8083) | sessions, lobbies, running games, asking the AI for moves, limits and load control | | Game engine | `wizard_server/game_logic/` (package `wizard_game`) | pure Python, no dependencies | library, installed into server and AI images | the rules of Wizard | | AI service | `wizard_ai/` | Python, `websockets` | one process, no listening port | chooses moves for AI seats; training tools | The game engine is deliberately a separate installable package. The server uses it to run games, and the AI uses the *same* code to train, so a model learns exactly the rules it will later play by. ## Network layout (production) ```mermaid flowchart TB subgraph internet U[Browser] end subgraph net1 ["network wizard_website"] CADDY1((Caddy)) WEB["wizard_website :8082"] end subgraph net2 ["network wizard_api"] CADDY2((Caddy)) SRV["wizard_server :8083"] end subgraph net3 ["network wizard_internal (no internet)"] SRV2[wizard_server] AI[wizard_ai] end U -->|"https://game.marvinkrausser.com/wizard/website/"| CADDY1 --> WEB U -->|"wss://game.marvinkrausser.com/wizard/api/ws"| CADDY2 --> SRV AI -->|"ws://wizard_server:8083/internal/ai"| SRV2 ``` - Caddy is one existing container attached to the external networks `wizard_website` and `wizard_api`; it is drawn twice only for readability. - `wizard_server` is attached to two networks (`wizard_api` and `wizard_internal`); `SRV` and `SRV2` are the same container. - Caddy **strips the path prefix**: the website container sees `/assets/…`, the server sees `/ws` and `/health`. - The AI service connects *to* the server. It has no open port, and its network (`wizard_internal`) has no internet access. ## How one move travels through the system A human plays a card in a game that also has an AI player: ```mermaid sequenceDiagram participant B as Browser (useWizard) participant S as Server (WizardService / Lobby) participant E as WizardGame (engine) participant A as AI service B->>S: {"type":"action","action":{"type":"play","card":17}} S->>E: game.apply(seat, PlayCard(card 17)) E-->>S: events (e.g. TrickCompleted) S-->>B: {"type":"state", lobby, game (personal observation), events} Note over S: next seat is an AI → Lobby._ai_loop S->>A: {"type":"decide","request_id":"…","observation":{…}} A->>A: agent.act(observation) A-->>S: {"type":"action","request_id":"…","action":{…}} Note over S: waits at least AI_MOVE_DELAY
(and TRICK_PAUSE after a trick) S->>E: game.apply(ai_seat, action) S-->>B: {"type":"state", …} ``` Important properties of this flow: 1. **The server is authoritative.** Browsers and the AI only *propose* actions; the engine validates every action (`WizardGame.apply` raises `IllegalActionError` for anything illegal). 2. **Every state message is complete.** The server does not send diffs. Each `state` message contains the full lobby and the full personal view of the game. The frontend simply replaces its state, which makes reconnects and reloads trivial. 3. **Each player receives their own view.** The server builds an `Observation` per seat, which never contains other players' hands (and hides bids while "hidden bids" is active). 4. **The AI sees exactly what a human sees.** The AI service receives the same `Observation` JSON a browser gets for that seat. ## Repository layout ``` . ├── compose.yaml # all three containers, networks, CPU/memory caps ├── .env.example # template for .env (secret, resource limits, capacity) ├── README.md # short project readme ├── docs/ # this documentation ├── instructions.txt # original task description ├── wizard_instructions/ # original rules (saved web page) │ ├── wizard_server/ │ ├── Dockerfile │ ├── requirements.txt # fastapi, uvicorn │ ├── requirements-dev.txt # + pytest, httpx, editable game_logic │ ├── app/ # the FastAPI server │ │ ├── main.py # app factory + routes │ │ ├── config.py # Settings from environment variables │ │ ├── protocol.py # browser → server message schema (pydantic) │ │ ├── service.py # WizardService: dispatches messages, sessions, cleanup │ │ ├── sessions.py # Session, Connection (ordered send queue), SessionRegistry │ │ ├── lobby.py # Lobby, Seat, LobbyManager, AI turn loop │ │ ├── ai_bridge.py # request/response with the AI service, fallback, circuit breaker │ │ ├── limits.py # TokenBucket, rate and connection limiters │ │ └── load.py # LoadMonitor: detects overload (lag, CPU, cgroup throttling) │ ├── tests/ # conftest.py/helpers.py (fixtures), test_server.py, │ │ # test_protection.py (limits/overload), test_limits.py (units) │ └── game_logic/ # installable package "wizard-game" │ ├── pyproject.toml │ ├── wizard_game/ │ │ ├── cards.py # Card, Suit, deck, card ids │ │ ├── actions.py # Bid, PlayCard, JSON (de)serialization │ │ ├── rules.py # stateless rule functions │ │ ├── state.py # Phase, GameOptions, results, events, RoundState │ │ ├── observation.py # Observation (a player's view) + JSON │ │ └── game.py # WizardGame state machine │ └── tests/ # rules + full random games │ ├── wizard_ai/ │ ├── Dockerfile # needs game_logic as extra build context │ ├── requirements.txt # websockets │ ├── models/ # trained checkpoints (mounted read-only) │ ├── wizard_ai/ │ │ ├── __main__.py # CLI: serve | train | evaluate │ │ ├── config.py │ │ ├── agents/ # Agent interface, random, heuristic, model agent │ │ ├── model/ # encoding.py (features/actions), policy.py (model interface) │ │ ├── training/ # env.py, self_play.py, evaluate.py │ │ └── service/client.py # production websocket client (workers, queue, degradation) │ └── tests/test_ai.py │ └── wizard_website/ ├── Dockerfile # node build → nginx ├── nginx.conf # listens on 8082, SPA fallback ├── vite.config.ts # base './', dev proxy for /api └── src/ ├── main.tsx, App.tsx, App.module.css ├── api/ # types.ts (protocol types), useWizard.ts (connection hook) ├── components/ # Button, PlayingCard, Modal, Toasts ├── screens/ # HomeScreen, LobbyScreen, GameScreen (+ its parts) └── util/cards.ts # sorting, labels, legal-action helpers ``` ## Key design decisions (and why) | Decision | Reason | |----------|--------| | Game rules in a dependency-free package | The AI trains on the exact same rules the server enforces; the engine is fast and easy to test in isolation. | | Engine works with seat indices only | Keeps it independent of users, names and networking. The server maps indices ↔ players. | | One `Observation` type for browser *and* AI | A single, well-tested definition of "what a player may know"; no risk of the AI accidentally seeing hidden information. | | Full-state messages instead of diffs | Simple client, trivial reconnects, no sync bugs. Messages are small (a few KB). | | Server asks the AI (request/response), AI seats live in the server | The AI service is stateless per request, can be scaled to several instances (round robin), and a missing AI never blocks a game (random fallback). | | All state in memory, single process | No database needed for a casual game. Consequence: a server restart ends all games, and the server must run with **one worker**. | | Session token in `sessionStorage` | Survives page reloads (reconnect to the same seat) but is per tab, so you can test several players in several tabs. | | Per-connection send queue | Websocket messages are always sent in order and never concurrently (see `sessions.Connection`). | | Relative asset paths + relative API URL in the frontend | The app works under any path prefix (`/wizard/website/` in production, `/` in development) without rebuilding. | | Refuse new games under load, never interrupt running ones | Players in a game are never punished for load; creating/starting games is what adds load, so that is what gets refused ("server busy"). Refusing sheds load by itself. See [chapter 9](09-limits-and-performance.md). | | Hard Docker caps plus software limits | The caps guarantee the host is protected even if a software limit fails; the software limits make overload visible and graceful instead of just slow. | | Health check stays "ok" under load | Docker must never restart an overloaded server — that would end every running game. |