added docs
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
# 8. Deployment, development & operations
|
||||
|
||||
## Docker
|
||||
|
||||
### Images
|
||||
|
||||
| Service | Dockerfile | Build context | Listens on | Notes |
|
||||
|---------|-----------|---------------|------------|-------|
|
||||
| `wizard_website` | `wizard_website/Dockerfile` | `wizard_website/` | 8082 (nginx) | multi-stage: node build → nginx; build arg `VITE_WS_URL` (optional) |
|
||||
| `wizard_server` | `wizard_server/Dockerfile` | `wizard_server/` | 8083 (uvicorn) | installs `requirements.txt`, then `game_logic/`; runs as non-root; health check on `/health` |
|
||||
| `wizard_ai` | `wizard_ai/Dockerfile` | `wizard_ai/` **plus** named context `game_logic` | – | installs the game engine from the extra context; runs `python -m wizard_ai serve` |
|
||||
|
||||
The AI image needs the game engine, which lives in the server folder. Compose
|
||||
passes it as an additional build context (requires docker compose ≥ 2.17):
|
||||
|
||||
```yaml
|
||||
build:
|
||||
context: ./wizard_ai
|
||||
additional_contexts:
|
||||
game_logic: ./wizard_server/game_logic
|
||||
```
|
||||
|
||||
Building it by hand: `docker build --build-context
|
||||
game_logic=wizard_server/game_logic wizard_ai`.
|
||||
|
||||
### Networks
|
||||
|
||||
| Network | Type | Members |
|
||||
|---------|------|---------|
|
||||
| `wizard_website` | external (shared with Caddy) | `wizard_website` |
|
||||
| `wizard_api` | external (shared with Caddy) | `wizard_server` |
|
||||
| `wizard_internal` | created by compose, `internal: true` (no internet) | `wizard_server`, `wizard_ai` |
|
||||
|
||||
No ports are published to the host; only Caddy reaches the containers.
|
||||
|
||||
### Environment (`.env` next to `compose.yaml`)
|
||||
|
||||
Compose reads `.env` automatically; `.env.example` is only a template and is
|
||||
not read by anything.
|
||||
|
||||
| Variable | Used by | Required | Meaning |
|
||||
|----------|---------|----------|---------|
|
||||
| `AI_SHARED_SECRET` | server + AI | **yes** (compose refuses to start without it) | shared secret for `/internal/ai` |
|
||||
| `ALLOWED_ORIGINS` | server | no | e.g. `https://game.marvinkrausser.com` |
|
||||
| `AI_AGENT` | AI | no (default `model`) | `model`, `heuristic` or `random` |
|
||||
| `LOG_LEVEL` | server + AI | no | `INFO`, `DEBUG`, … |
|
||||
|
||||
Further server settings (`AI_MOVE_DELAY`, `TRICK_PAUSE`, …, see
|
||||
"Configuration" in [the server chapter](04-server.md)) can be added to
|
||||
the `environment:` section of `wizard_server` in `compose.yaml`.
|
||||
|
||||
### AI models
|
||||
|
||||
`./wizard_ai/models` is mounted read-only to `/srv/models`; the service loads
|
||||
`/srv/models/latest/` (a policy checkpoint directory). Put a new checkpoint
|
||||
there and restart `wizard_ai` — no rebuild needed. Without a checkpoint the
|
||||
heuristic agent is used.
|
||||
|
||||
### Deploying
|
||||
|
||||
```bash
|
||||
cp .env.example .env # once; set AI_SHARED_SECRET
|
||||
docker compose up -d --build # build and (re)start everything
|
||||
docker compose up -d --build wizard_website # only one service
|
||||
docker compose logs -f wizard_server
|
||||
docker compose ps # server shows "healthy" when /health answers
|
||||
```
|
||||
|
||||
## Reverse proxy (Caddy)
|
||||
|
||||
The existing Caddy container serves the game under `game.marvinkrausser.com`:
|
||||
|
||||
```caddy
|
||||
game.marvinkrausser.com {
|
||||
import common
|
||||
|
||||
redir /wizard /wizard/website/ permanent
|
||||
redir /wizard/ /wizard/website/ permanent
|
||||
redir /wizard/website /wizard/website/ permanent
|
||||
|
||||
@wizard_api path /wizard/api /wizard/api/*
|
||||
handle @wizard_api {
|
||||
uri strip_prefix /wizard/api
|
||||
reverse_proxy wizard_server:8083
|
||||
}
|
||||
|
||||
handle_path /wizard/website/* {
|
||||
reverse_proxy wizard_website:8082
|
||||
}
|
||||
|
||||
handle {
|
||||
respond 404
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
What each part means for the app:
|
||||
|
||||
- The website must be opened with a trailing slash (`/wizard/website/`) so
|
||||
relative asset paths resolve — the redirects take care of that.
|
||||
- Both prefixes are stripped, so the containers do not know about `/wizard/…`.
|
||||
The frontend finds the API through the relative URL `../api/ws`.
|
||||
- Websockets are proxied automatically by Caddy.
|
||||
- `/wizard/api/internal/ai` is reachable through this block, but the server
|
||||
refuses it because the request carries Caddy's forwarding headers.
|
||||
|
||||
URLs: game `https://game.marvinkrausser.com/wizard/website/`, health check
|
||||
`https://game.marvinkrausser.com/wizard/api/health`.
|
||||
|
||||
## Local development
|
||||
|
||||
One Python virtual environment for server and AI:
|
||||
|
||||
```bash
|
||||
python -m venv .venv && . .venv/bin/activate
|
||||
pip install -r wizard_server/requirements-dev.txt -r wizard_ai/requirements-dev.txt
|
||||
```
|
||||
|
||||
`requirements-dev.txt` installs the game engine **editable** (`-e`), so changes
|
||||
to `wizard_game` are picked up immediately.
|
||||
|
||||
Run the three parts in three terminals:
|
||||
|
||||
```bash
|
||||
# 1. server on :8000 (add AI_MOVE_DELAY=0 TRICK_PAUSE=0 to make AI players instant)
|
||||
cd wizard_server && AI_SHARED_SECRET=dev uvicorn app.main:app --reload
|
||||
|
||||
# 2. AI service
|
||||
cd wizard_ai && AI_SHARED_SECRET=dev SERVER_URL=ws://localhost:8000/internal/ai python -m wizard_ai serve
|
||||
|
||||
# 3. website on :5173 (proxies /api → localhost:8000 without the /api prefix)
|
||||
cd wizard_website && npm install && npm run dev
|
||||
```
|
||||
|
||||
Open <http://localhost:5173>. To play as several humans, use several browser
|
||||
tabs (each tab is its own session). Without step 2 the game still works; AI
|
||||
seats then play random moves.
|
||||
|
||||
## Tests and checks
|
||||
|
||||
```bash
|
||||
cd wizard_server && pytest # game engine (game_logic/tests) + server (tests/)
|
||||
cd wizard_ai && pytest
|
||||
cd wizard_website && npm run lint && npm run build
|
||||
```
|
||||
|
||||
| Suite | Count | Covers |
|
||||
|-------|-------|--------|
|
||||
| game engine | 52 | rules, full random games for 3–6 players, invariants, JSON |
|
||||
| server | 9 | websocket flows incl. a complete game, reconnect, AI endpoint security |
|
||||
| AI | 13 | encoding, agents, env, trainer, checkpoints |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause / fix |
|
||||
|---------|-------------|
|
||||
| Browser console: *Refused to apply style … MIME type ('')*, 404 for `/assets/…` | The page loads assets from the wrong path. The build must use relative paths (`base: './'` in `vite.config.ts`) and the page must be opened with a trailing slash. Rebuild the website image after changing the Vite config. |
|
||||
| Red banner "Connection lost. Reconnecting…" forever | The websocket URL does not reach the server. Check `https://…/wizard/api/health`; check that the Caddy block strips `/wizard/api`; check `ALLOWED_ORIGINS` (must contain the exact page origin, e.g. `https://game.marvinkrausser.com`). |
|
||||
| Banner "This game was opened in another window" | The same session (token) was opened in a second tab; reload this tab to take it back. |
|
||||
| Lobby shows "The AI service is offline" | `wizard_ai` is not connected. `docker compose logs wizard_ai`: *Server rejected the connection, check AI_SHARED_SECRET* → secrets differ; connection refused → server not running or wrong `SERVER_URL`. |
|
||||
| AI players only make weird/random moves | Same as above (random fallback), or the AI answers too slowly (`AI_DECISION_TIMEOUT`), see server logs "Playing random move for player …". |
|
||||
| Game waits and nothing happens | It is a disconnected human's turn (seat shows "offline"). The game waits until they reconnect or their session expires (`SESSION_TTL`, then an AI takes over). |
|
||||
| All lobbies gone | The server restarted — state is in memory only. |
|
||||
| Tests pass but behaviour does not change after editing `wizard_game` | A non-editable copy of `wizard_game` is installed in your virtualenv and shadows the source. `pip uninstall wizard-game && pip install -e wizard_server/game_logic`. Check with `python -c "import wizard_game; print(wizard_game.__file__)"`. |
|
||||
| Server tests hang | A test opened a websocket without a context manager; use `client.sockets.enter_context(client.websocket_connect(...))`. |
|
||||
|
||||
## Recipes
|
||||
|
||||
### Add a new game option (e.g. a rule variant)
|
||||
|
||||
1. `wizard_game/state.py`: add a field to `GameOptions` and to `to_dict` /
|
||||
`from_dict`.
|
||||
2. `wizard_game/game.py`: implement the behaviour using
|
||||
`self.options.<field>`; add tests in `game_logic/tests/test_game.py`.
|
||||
3. `wizard_server/app/protocol.py`: add the field to `SetOptions`;
|
||||
`service.py`: pass it into `GameOptions(...)`.
|
||||
4. `wizard_website/src/api/types.ts`: add it to `GameOptions` and the
|
||||
`set_options` message; `LobbyScreen.tsx`: add a checkbox (copy the
|
||||
"Hidden bids" one).
|
||||
5. `wizard_ai/wizard_ai/model/encoding.py`: add a feature (and update
|
||||
`FEATURE_SIZE`) if the AI should know about it; `__main__.py`: add a
|
||||
`--flag` to `train`.
|
||||
|
||||
### Add a new client message
|
||||
|
||||
1. `protocol.py`: new pydantic model, add it to the `ClientMessage` union.
|
||||
2. `service.py`: handle it in `_dispatch` (usually by calling a new `Lobby`
|
||||
method and then `lobby.broadcast()`).
|
||||
3. `types.ts`: add it to `ClientMessage`; `useWizard.ts`: add a function to
|
||||
`actions`.
|
||||
4. Add a test in `wizard_server/tests/test_server.py`.
|
||||
|
||||
### Change how fast AI players move
|
||||
|
||||
Set `AI_MOVE_DELAY` (per move) and `TRICK_PAUSE` (after a trick) for
|
||||
`wizard_server` in `compose.yaml`.
|
||||
|
||||
### Add another AI agent
|
||||
|
||||
Subclass `Agent` in `wizard_ai/wizard_ai/agents/`, add it to `AGENT_NAMES`
|
||||
and `make_agent` in `agents/__init__.py`, then use it via `AI_AGENT=<name>` or
|
||||
in `evaluate --agents`.
|
||||
|
||||
### Change container ports
|
||||
|
||||
Website: `listen` in `wizard_website/nginx.conf` and `EXPOSE` in its
|
||||
Dockerfile. Server: `--port`, `EXPOSE` and the health check URL in
|
||||
`wizard_server/Dockerfile`, and `SERVER_URL` of `wizard_ai` in `compose.yaml`.
|
||||
Then update the Caddy block.
|
||||
Reference in New Issue
Block a user