Files
Wizard/docs/05-protocol.md
T
2026-09-27 11:26:10 +02:00

169 lines
6.3 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.
#### `error`
```json
{"type": "error", "message": "Only the host can do that"}
```
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`.
### 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 |
| 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>","observation":{…}}` | choose an action for `observation.player` |
| AI → server | `{"type":"action","request_id":"<same>","action":{"type":"play","card":17}}` | the chosen action |
| AI → server | `{"type":"error","request_id":"<same>","message":"…"}` | the AI failed; 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.
- Several AI service instances may connect; the server distributes requests
round robin.