Files
marvinandClaude Opus 5.5 21a2b044d9 Trump card: draw from the stack, no trump for wizards and jesters
- Deal the hands from the shuffled stack and draw the trump card from it,
  so the trump card is in nobody's hand and cannot be played
- A wizard or jester as trump card means no trump suit (previously a
  wizard let the dealer choose); remove the trump selection phase, the
  ChooseTrump action, the trump picker UI and the AI trump actions
- Completed tricks carry their round's trump card, so the table keeps
  showing the matching trump card while the last trick of a round is
  displayed
- Card corners are no longer rotated, so 6 and 9 cannot be confused
- Trump label explains why there is no trump (wizard, jester, last round)
- Tests: trump card never dealt, special cards give no trump, tricks keep
  their trump card

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 10:28:53 +02:00

167 lines
4.7 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):
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
# The trump card of the round the trick was played in.
trump_card: Card | None = None
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,
"trump_card": self.trump_card.to_dict() if self.trump_card else None,
}
@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"],
trump_card=card_by_id(data["trump_card"]["id"]) if data.get("trump_card") else None,
)
@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)