Files
Wizard/wizard_server/game_logic/wizard_game/state.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

164 lines
4.5 KiB
Python

"""Value types describing game state, options, results and events."""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Any
from .cards import Card, Suit, card_by_id
class Phase(str, Enum):
TRUMP_SELECTION = "trump_selection"
BIDDING = "bidding"
PLAYING = "playing"
FINISHED = "finished"
@dataclass(frozen=True, slots=True)
class GameOptions:
# Note: the sum of all bids never equals the number of tricks of a round;
# this is a fixed rule (see ``WizardGame._allowed_bids``), not an option.
# "hiddentip": bids are revealed only once every player has bid.
hidden_bids: bool = False
def to_dict(self) -> dict:
return {"hidden_bids": self.hidden_bids}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> GameOptions:
return cls(hidden_bids=bool(data.get("hidden_bids", False)))
@dataclass(frozen=True, slots=True)
class PlayedCard:
player: int
card: Card
def to_dict(self) -> dict:
return {"player": self.player, "card": self.card.to_dict()}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> PlayedCard:
return cls(player=data["player"], card=card_by_id(data["card"]["id"]))
@dataclass(frozen=True, slots=True)
class TrickResult:
round_number: int
trick_number: int
cards: tuple[PlayedCard, ...]
winner: int
def to_dict(self) -> dict:
return {
"round_number": self.round_number,
"trick_number": self.trick_number,
"cards": [played.to_dict() for played in self.cards],
"winner": self.winner,
}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> TrickResult:
return cls(
round_number=data["round_number"],
trick_number=data["trick_number"],
cards=tuple(PlayedCard.from_dict(c) for c in data["cards"]),
winner=data["winner"],
)
@dataclass(frozen=True, slots=True)
class RoundResult:
round_number: int
bids: tuple[int, ...]
tricks: tuple[int, ...]
score_changes: tuple[int, ...]
scores: tuple[int, ...]
def to_dict(self) -> dict:
return {
"round_number": self.round_number,
"bids": list(self.bids),
"tricks": list(self.tricks),
"score_changes": list(self.score_changes),
"scores": list(self.scores),
}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> RoundResult:
return cls(
round_number=data["round_number"],
bids=tuple(data["bids"]),
tricks=tuple(data["tricks"]),
score_changes=tuple(data["score_changes"]),
scores=tuple(data["scores"]),
)
# --- Events -----------------------------------------------------------------
# ``WizardGame.apply`` returns the events caused by an action. They are handy
# for clients that want to animate what happened (e.g. pause after a trick).
@dataclass(frozen=True, slots=True)
class RoundStarted:
round_number: int
dealer: int
trump_card: Card | None
def to_dict(self) -> dict:
return {
"type": "round_started",
"round_number": self.round_number,
"dealer": self.dealer,
"trump_card": self.trump_card.to_dict() if self.trump_card else None,
}
@dataclass(frozen=True, slots=True)
class TrickCompleted:
trick: TrickResult
def to_dict(self) -> dict:
return {"type": "trick_completed", "trick": self.trick.to_dict()}
@dataclass(frozen=True, slots=True)
class RoundCompleted:
result: RoundResult
def to_dict(self) -> dict:
return {"type": "round_completed", "result": self.result.to_dict()}
@dataclass(frozen=True, slots=True)
class GameFinished:
scores: tuple[int, ...]
winners: tuple[int, ...]
def to_dict(self) -> dict:
return {"type": "game_finished", "scores": list(self.scores), "winners": list(self.winners)}
Event = RoundStarted | TrickCompleted | RoundCompleted | GameFinished
@dataclass(slots=True)
class RoundState:
"""Mutable per-round state, owned by ``WizardGame``."""
round_number: int
dealer: int
hands: list[list[Card]]
trump_card: Card | None
trump_suit: Suit | None
bids: list[int | None]
tricks_won: list[int]
current_trick: list[PlayedCard] = field(default_factory=list)
trick_leader: int = 0
trick_number: int = 1
played_cards: list[Card] = field(default_factory=list)