- 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>
8.2 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.
server_status
Sent right after welcome and to every connected client whenever the server's
load level or capacity changes.
{"type": "server_status", "load": "overloaded", "accepting_games": false}
load:normal,busy(informational) oroverloaded.accepting_games:falsewhile the server refusescreate_lobbyandstart_game(overloaded, orMAX_ACTIVE_GAMESgames running). Everything else, including running games, keeps working.
error
{"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
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>","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 whosetimeoutpassed before it could start them (expired). - The server stops asking an AI connection that already has
AI_MAX_INFLIGHTunanswered requests, and pauses the AI after repeated real failures (circuit breaker).overloaded/expiredanswers are load shedding, not failures, and do not trip the breaker. See "Layer 4" in chapter 9. - Several AI service instances may connect; the server distributes requests round robin.