# 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 ` 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":"","observation":{…}}` | choose an action for `observation.player` | | AI → server | `{"type":"action","request_id":"","action":{"type":"play","card":17}}` | the chosen action | | AI → server | `{"type":"error","request_id":"","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.