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

6.3 KiB
Raw Blame History

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.


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.

{"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 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

{"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.