"""Messages exchanged with web clients over the ``/ws`` websocket. Client -> server messages are validated with pydantic. Server -> client messages are plain dicts: - ``{"type": "welcome", "player_id", "token", "name"}`` - ``{"type": "state", "lobby": LobbyView | null, "game": GameView | null, "events": [...]}`` - ``{"type": "error", "message", "code"?}`` - ``{"type": "server_status", "load", "accepting_games"}`` """ from __future__ import annotations from typing import Annotated, Any, Literal from pydantic import BaseModel, Field, StringConstraints, TypeAdapter PlayerName = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=20)] LobbyCode = Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, min_length=4, max_length=12)] class Hello(BaseModel): type: Literal["hello"] # Token of a previous session, used to reconnect after a page reload. token: str | None = None class CreateLobby(BaseModel): type: Literal["create_lobby"] name: PlayerName class JoinLobby(BaseModel): type: Literal["join_lobby"] code: LobbyCode name: PlayerName class LeaveLobby(BaseModel): type: Literal["leave_lobby"] class AddAI(BaseModel): type: Literal["add_ai"] class RemovePlayer(BaseModel): type: Literal["remove_player"] player_id: str class SetOptions(BaseModel): type: Literal["set_options"] hidden_bids: bool class StartGame(BaseModel): type: Literal["start_game"] class GameAction(BaseModel): type: Literal["action"] action: dict[str, Any] class ReturnToLobby(BaseModel): type: Literal["return_to_lobby"] ClientMessage = Annotated[ Hello | CreateLobby | JoinLobby | LeaveLobby | AddAI | RemovePlayer | SetOptions | StartGame | GameAction | ReturnToLobby, Field(discriminator="type"), ] client_message_adapter: TypeAdapter[ClientMessage] = TypeAdapter(ClientMessage) # Machine readable error codes (the frontend reacts to them). ERROR_BUSY = "busy" # server has no capacity for this right now ERROR_RATE_LIMITED = "rate_limited" # client sends too many messages / creates too many lobbies # Websocket close code "Try Again Later" (RFC 6455 registry), used when full. CLOSE_TRY_AGAIN_LATER = 1013 def error(message: str, code: str | None = None) -> dict: message_dict: dict = {"type": "error", "message": message} if code: message_dict["code"] = code return message_dict