"""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"}`` """ 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"] not_equal: bool 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) def error(message: str) -> dict: return {"type": "error", "message": message}