The sum of all bids may never equal the number of cards each player holds in the round; the last bidder has to adjust their bid. This was an optional "not equal" lobby setting and is now a fixed rule of the game engine, so it applies to humans, AI players and training alike. - Remove GameOptions.not_equal, the set_options field, the lobby toggle and the --not-equal training flag - Explain the rule in the lobby and show the blocked bid while bidding - Drop the option from the AI observation encoding - Tests check that no round ends with bids summing to the cards in hand Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
80 lines
1.9 KiB
Python
80 lines
1.9 KiB
Python
"""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"]
|
|
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}
|