Files
Wizard/docs/05-protocol.md
T
marvinandClaude Opus 5.5 9ffee8b952 Document limits, load control and performance
- New docs/09-limits-and-performance.md: principles, all protection
  layers, what players see, configuration reference, measurements,
  tuning guide, monitoring and how to test
- Server chapter summarises the limits and links to chapter 9; circuit
  breaker description and AI_MAX_INFLIGHT default corrected
- Architecture: new files in the layout, design decisions for load
  handling; docs index lists chapter 9
- Protocol, AI and operations chapters link to chapter 9; test counts
- Root and component READMEs describe the limits and settings

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:04:21 +02:00

201 lines
8.2 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 real failures (circuit
breaker). `overloaded` / `expired` answers are load shedding, not failures,
and do not trip the breaker. See
"Layer 4" in [chapter 9](09-limits-and-performance.md).
- Several AI service instances may connect; the server distributes requests
round robin.