6.3 KiB
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 URLwss://game.marvinkrausser.com/wizard/api/ws). Schema:wizard_server/app/protocol.py(Python) andwizard_website/src/api/types.ts(TypeScript). Keep both in sync. - AI protocol — AI service ↔ server on
/internal/ai. Implemented inwizard_server/app/ai_bridge.pyandwizard_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.
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
typeor wrong field types →errormessage, connection stays open.
Server → browser
welcome
Sent after every hello.
{"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.
{
"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 seatiin the game (observationuses the same indices).eventslists what the last action caused:trick_completed,round_completed,round_started,game_finished. Usually empty.ai_availableisfalsewhen no AI service is connected; AI seats then play random moves.
error
{"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
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).
| 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.