Files
Wizard/wizard_server/app/protocol.py
T
marvinandClaude Opus 5.5 fde8e2fb2e Always forbid bids that add up to the cards in hand
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>
2026-09-27 10:15:25 +02:00

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}