"""The view of the game a single player is allowed to see. An ``Observation`` contains everything a player knows at a point in time and nothing more (no opponent hands, hidden bids stay hidden). It is used - by the server to send the game state to a web client, and - as the input of AI players, both in training and in production. ``to_dict`` / ``from_dict`` provide a lossless JSON representation. """ from __future__ import annotations from dataclasses import dataclass from typing import Any from .actions import Action, action_from_dict from .cards import Card, Suit, card_by_id from .state import GameOptions, Phase, PlayedCard, RoundResult, TrickResult @dataclass(frozen=True, slots=True) class Observation: player: int num_players: int options: GameOptions phase: Phase round_number: int total_rounds: int dealer: int current_player: int | None hand: tuple[Card, ...] hand_sizes: tuple[int, ...] trump_card: Card | None trump_suit: Suit | None # ``None`` for players who have not bid yet, or whose bid is still hidden. bids: tuple[int | None, ...] has_bid: tuple[bool, ...] tricks_won: tuple[int, ...] scores: tuple[int, ...] trick_number: int trick_leader: int current_trick: tuple[PlayedCard, ...] lead_suit: Suit | None # All cards played in the current round, including the current trick. played_cards: tuple[Card, ...] last_trick: TrickResult | None round_history: tuple[RoundResult, ...] # Empty unless it is this player's turn. legal_actions: tuple[Action, ...] winners: tuple[int, ...] @property def is_my_turn(self) -> bool: return self.current_player == self.player def to_dict(self) -> dict: return { "player": self.player, "num_players": self.num_players, "options": self.options.to_dict(), "phase": self.phase.value, "round_number": self.round_number, "total_rounds": self.total_rounds, "dealer": self.dealer, "current_player": self.current_player, "hand": [card.to_dict() for card in self.hand], "hand_sizes": list(self.hand_sizes), "trump_card": self.trump_card.to_dict() if self.trump_card else None, "trump_suit": self.trump_suit.value if self.trump_suit else None, "bids": list(self.bids), "has_bid": list(self.has_bid), "tricks_won": list(self.tricks_won), "scores": list(self.scores), "trick_number": self.trick_number, "trick_leader": self.trick_leader, "current_trick": [played.to_dict() for played in self.current_trick], "lead_suit": self.lead_suit.value if self.lead_suit else None, "played_cards": [card.to_dict() for card in self.played_cards], "last_trick": self.last_trick.to_dict() if self.last_trick else None, "round_history": [result.to_dict() for result in self.round_history], "legal_actions": [action.to_dict() for action in self.legal_actions], "winners": list(self.winners), } @classmethod def from_dict(cls, data: dict[str, Any]) -> Observation: def card(value: dict | None) -> Card | None: return card_by_id(value["id"]) if value else None def suit(value: str | None) -> Suit | None: return Suit(value) if value else None return cls( player=data["player"], num_players=data["num_players"], options=GameOptions.from_dict(data["options"]), phase=Phase(data["phase"]), round_number=data["round_number"], total_rounds=data["total_rounds"], dealer=data["dealer"], current_player=data["current_player"], hand=tuple(card_by_id(c["id"]) for c in data["hand"]), hand_sizes=tuple(data["hand_sizes"]), trump_card=card(data["trump_card"]), trump_suit=suit(data["trump_suit"]), bids=tuple(data["bids"]), has_bid=tuple(data["has_bid"]), tricks_won=tuple(data["tricks_won"]), scores=tuple(data["scores"]), trick_number=data["trick_number"], trick_leader=data["trick_leader"], current_trick=tuple(PlayedCard.from_dict(p) for p in data["current_trick"]), lead_suit=suit(data["lead_suit"]), played_cards=tuple(card_by_id(c["id"]) for c in data["played_cards"]), last_trick=TrickResult.from_dict(data["last_trick"]) if data["last_trick"] else None, round_history=tuple(RoundResult.from_dict(r) for r in data["round_history"]), legal_actions=tuple(action_from_dict(a) for a in data["legal_actions"]), winners=tuple(data["winners"]), )