Files
Wizard/docs/05-protocol.md
T
marvinandClaude Opus 5.5 87dfcd55b7 Add resource limits, abuse protection and overload handling
Running games always continue; new work is refused when there is no
capacity; abuse is rejected immediately.

- Docker: CPU/memory/pids caps per container (overridable in .env);
  uvicorn message size, queue and concurrency limits
- Server abuse limits: connections total/per IP (close 1013), message
  rate per connection (token bucket, close 1008 on floods), lobby
  creation rate per IP
- Load monitor: event loop lag, CPU of the own cgroup quota and
  throttling, with hysteresis (normal/busy/overloaded, severe lag
  overloads immediately)
- Admission control: create_lobby and start_game refused while
  overloaded or MAX_ACTIVE_GAMES are running; server_status message
  broadcast on changes; /health reports load but stays ok
- AI bridge: timeout sent with requests, in-flight cap, circuit breaker
- AI service: bounded workers and queue ("overloaded"), drops expired
  requests, degrades to the heuristic agent when busy or too slow
- Website: busy notices, Create/Start disabled when not accepting
  games, slow retry when the server is full
- Tests for limiters, load levels, admission control and AI service
  limits; docs updated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 12:51:46 +02:00

200 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 5. Protocol reference
All messages are JSON objects with a `type` field, sent as websocket text
frames. There are two protocols:
- **Client protocol** — browser ↔ server on `/ws`
(public URL `wss://game.marvinkrausser.com/wizard/api/ws`).
Schema: `wizard_server/app/protocol.py` (Python) and
`wizard_website/src/api/types.ts` (TypeScript). Keep both in sync.
- **AI protocol** — AI service ↔ server on `/internal/ai`.
Implemented in `wizard_server/app/ai_bridge.py` and
`wizard_ai/wizard_ai/service/client.py`.
Game objects inside messages (cards, actions, observations, events) use the
engine's JSON format described in [the engine chapter](03-game-engine.md#json-format).
---
## Client protocol
### Browser → server
| `type` | Fields | Allowed when | Effect |
|--------|--------|--------------|--------|
| `hello` | `token: string \| null` | always; must be the first message | starts or resumes a session |
| `create_lobby` | `name` | after hello | creates a lobby and joins it as host |
| `join_lobby` | `code`, `name` | after hello | joins an existing lobby |
| `leave_lobby` | – | in a lobby | leaves (during a game an AI takes the seat) |
| `add_ai` | – | host, lobby waiting, < 6 seats | adds an AI seat |
| `remove_player` | `player_id` | host, lobby waiting | removes a seat (human or AI) |
| `set_options` | `hidden_bids: bool` | host, lobby waiting | changes game options |
| `start_game` | – | host, lobby waiting, 3–6 seats | starts the game |
| `action` | `action: {"type":"bid","value":n}` or `{"type":"play","card":id}` | your turn in a running game | performs a move |
| `return_to_lobby` | – | host, game finished | back to the waiting lobby |
Validation (`protocol.py`):
- `name`: whitespace stripped, 1–20 characters.
- `code`: whitespace stripped, upper-cased, 4–12 characters.
- Unknown `type` or wrong field types → `error` message, connection stays open.
### Server → browser
#### `welcome`
Sent after every `hello`.
```json
{"type": "welcome", "player_id": "3f9c0a1b2d4e", "token": "Zq…", "name": "Marvin"}
```
The browser stores `token` (in `sessionStorage`) and sends it in the next
`hello` to resume the same session. `player_id` identifies you in
`lobby.players` and `lobby.host_id`.
#### `state`
Sent after `hello` and whenever anything in your lobby changes. It always
contains the **complete** current state.
```json
{
"type": "state",
"lobby": {
"code": "7C7TAJ",
"status": "playing",
"host_id": "3f9c0a1b2d4e",
"options": {"hidden_bids": false},
"players": [
{"id": "3f9c0a1b2d4e", "name": "Marvin", "is_ai": false, "connected": true},
{"id": "ai-1a2b3c4d", "name": "Merlin", "is_ai": true, "connected": true},
{"id": "ai-5e6f7a8b", "name": "Morgana", "is_ai": true, "connected": true}
],
"min_players": 3,
"max_players": 6,
"ai_available": true
},
"game": {
"you": 0,
"observation": { "…": "the personal Observation of seat 0" }
},
"events": [
{"type": "trick_completed", "trick": {"…": "…"}}
]
}
```
- `lobby: null, game: null` → you are not in a lobby (start page).
- `game: null` → lobby is waiting (no game running).
- `players[i]` is seat `i` in the game (`observation` uses the same indices).
- `events` lists what the last action caused: `trick_completed`,
`round_completed`, `round_started`, `game_finished`. Usually empty.
- `ai_available` is `false` when no AI service is connected; AI seats then play
random moves.
#### `server_status`
Sent right after `welcome` and to every connected client whenever the server's
load level or capacity changes.
```json
{"type": "server_status", "load": "overloaded", "accepting_games": false}
```
- `load`: `normal`, `busy` (informational) or `overloaded`.
- `accepting_games`: `false` while the server refuses `create_lobby` and
`start_game` (overloaded, or `MAX_ACTIVE_GAMES` games running). Everything
else, including running games, keeps working.
#### `error`
```json
{"type": "error", "message": "Only the host can do that"}
{"type": "error", "message": "The server is busy right now, …", "code": "busy"}
```
`code` is present for errors the client may react to:
| `code` | Meaning |
|--------|---------|
| `busy` | no capacity: connection limit reached (followed by close 1013), or `create_lobby` / `start_game` refused because the server does not accept new games |
| `rate_limited` | too many messages on this connection, or too many lobbies created from this IP |
Sent only to the client that caused it. Typical messages: `Send 'hello' first`,
`Invalid message: …` (followed by the pydantic reason),
`Lobby ABC123 does not exist`, `The game in this lobby has already started`,
`This lobby is full`, `Only the host can do that`,
`Wizard needs 3 to 6 players`, `Illegal action Bid(value=2) for player 0`,
`It is not player 1's turn`, `You were removed from the lobby`,
`This session was opened in another window`, `Too many messages, slow down.`,
`You are creating lobbies too quickly, please wait a moment.`
### Close codes
| Code | Meaning | Frontend reaction |
|------|---------|-------------------|
| (handshake rejected) | origin not in `ALLOWED_ORIGINS`; the server closes with 1008 before accepting, so the browser sees a failed connection | reconnects with backoff |
| 1008 | message flood (too many dropped messages) | reconnects with backoff |
| 1009 | message larger than 64 KB (uvicorn `--ws-max-size`) | reconnects with backoff |
| 1013 | "Try Again Later": connection limit reached (preceded by an `error` with code `busy`) | banner "The server is full", retries slowly (15 s, 30 s, 60 s …) |
| 4000 | the session was opened in another tab | stops reconnecting, shows a banner |
| other | network problem, server restart | reconnects (1 s, 2 s, 4 s … max 10 s) |
### Example conversation
```mermaid
sequenceDiagram
participant H as Host browser
participant S as Server
participant G as Guest browser
H->>S: hello {token:null}
S-->>H: welcome, state {lobby:null}
H->>S: create_lobby {name:"Ann"}
S-->>H: state {lobby: code "K7M2QX", players:[Ann]}
G->>S: hello {token:null}
S-->>G: welcome, state {lobby:null}
G->>S: join_lobby {code:"k7m2qx", name:"Ben"}
S-->>H: state {players:[Ann, Ben]}
S-->>G: state {players:[Ann, Ben]}
H->>S: add_ai
S-->>H: state {players:[Ann, Ben, Merlin]}
S-->>G: state {…}
H->>S: start_game
S-->>H: state {game:{you:0, observation{phase:"bidding", current_player:1}}}
S-->>G: state {game:{you:1, …}}
G->>S: action {type:"bid", value:0}
S-->>H: state {…}
S-->>G: state {…}
```
---
## AI protocol
The AI service connects to `ws://wizard_server:8083/internal/ai` with the
header `Authorization: Bearer <AI_SHARED_SECRET>` and without forwarding
headers (see "Routes" in [the server chapter](04-server.md)).
| Direction | Message | Meaning |
|-----------|---------|---------|
| AI → server | `{"type":"hello","agent":"heuristic"}` | informational, logged |
| server → AI | `{"type":"decide","request_id":"<hex>","timeout":5.0,"observation":{…}}` | choose an action for `observation.player`; the server waits at most `timeout` seconds |
| AI → server | `{"type":"action","request_id":"<same>","action":{"type":"play","card":17},"agent":"heuristic"}` | the chosen action; `agent` names the agent that decided (logged) |
| AI → server | `{"type":"error","request_id":"<same>","message":"…"}` | the AI failed or refused (`overloaded`, `expired`); the server plays a random move |
Rules:
- Requests are independent; the AI service may answer them in any order
(matched by `request_id`) and may handle several concurrently.
- The answer must be one of `observation.legal_actions`; otherwise the server
plays a random legal move.
- No answer within `AI_DECISION_TIMEOUT` (5 s) → random move. The AI service
drops requests whose `timeout` passed before it could start them (`expired`).
- The server stops asking an AI connection that already has `AI_MAX_INFLIGHT`
unanswered requests, and pauses the AI after repeated failures (circuit
breaker); see "Limits and overload handling" in
[the server chapter](04-server.md).
- Several AI service instances may connect; the server distributes requests
round robin.