from __future__ import annotations from dataclasses import dataclass, field from time import time from typing import Any from texas_holdem.cards import Card from texas_holdem.evaluator import HandValue @dataclass(slots=True) class PlayerAction: action: str amount: int = 0 @classmethod def from_dict(cls, payload: dict[str, Any]) -> "PlayerAction": return cls(str(payload.get("action", "")).lower(), int(payload.get("amount") or 0)) def to_dict(self) -> dict[str, object]: return {"action": self.action, "amount": self.amount} @dataclass(slots=True) class PlayerState: player_id: str name: str stack: int seat: int hole_cards: list[Card] = field(default_factory=list) folded: bool = False all_in: bool = False in_hand: bool = False street_bet: int = 0 total_bet: int = 0 def reset_for_hand(self) -> None: self.hole_cards = [] self.folded = False self.all_in = False self.in_hand = self.stack > 0 self.street_bet = 0 self.total_bet = 0 def reset_for_street(self) -> None: self.street_bet = 0 def commit(self, amount: int) -> int: committed = max(0, min(amount, self.stack)) self.stack -= committed self.street_bet += committed self.total_bet += committed if self.stack == 0 and self.in_hand and not self.folded: self.all_in = True return committed def public_dict(self) -> dict[str, object]: return { "player_id": self.player_id, "name": self.name, "seat": self.seat, "stack": self.stack, "folded": self.folded, "all_in": self.all_in, "in_hand": self.in_hand, "street_bet": self.street_bet, "total_bet": self.total_bet, } @dataclass(slots=True) class ActionRecord: hand_number: int street: str player_id: str action: str amount: int street_bet: int stack: int def to_dict(self) -> dict[str, object]: return { "hand_number": self.hand_number, "street": self.street, "player_id": self.player_id, "action": self.action, "amount": self.amount, "street_bet": self.street_bet, "stack": self.stack, } @dataclass(slots=True) class Observation: game_id: str hand_number: int street: str player_id: str seat: int button_seat: int small_blind: int big_blind: int board: list[Card] hole_cards: list[Card] players: list[dict[str, object]] pot: int to_call: int min_raise_to: int | None legal_actions: list[dict[str, object]] action_history: list[ActionRecord] def to_dict(self) -> dict[str, object]: return { "game_id": self.game_id, "hand_number": self.hand_number, "street": self.street, "player_id": self.player_id, "seat": self.seat, "button_seat": self.button_seat, "small_blind": self.small_blind, "big_blind": self.big_blind, "board": [str(card) for card in self.board], "hole_cards": [str(card) for card in self.hole_cards], "players": self.players, "pot": self.pot, "to_call": self.to_call, "min_raise_to": self.min_raise_to, "legal_actions": self.legal_actions, "action_history": [record.to_dict() for record in self.action_history], } @dataclass(slots=True) class BlindLevel: """A snapshot of the blind configuration that took effect at a given hand. The structure is intentionally append-only: every time the blinds change (or the very first hand seeds the initial values) we push a new ``BlindLevel`` so callers can reconstruct how the stakes evolved over the course of the game without losing any prior state. """ hand_number: int small_blind: int big_blind: int def to_dict(self) -> dict[str, object]: return { "hand_number": self.hand_number, "small_blind": self.small_blind, "big_blind": self.big_blind, } @dataclass(slots=True) class PotAward: amount: int winners: list[str] hand_value: HandValue | None def to_dict(self) -> dict[str, object]: return { "amount": self.amount, "winners": self.winners, "hand_value": self.hand_value.to_dict() if self.hand_value else None, } @dataclass(slots=True) class HandSummary: game_id: str hand_number: int button_seat: int board: list[Card] actions: list[ActionRecord] awards: list[PotAward] # ``blinds`` records the exact blind level used by this hand. Storing it # on the summary (rather than only on the game) guarantees historical # hands remain self-describing even after the blinds are raised later. blinds: BlindLevel | None = None hole_cards: dict[str, list[Card]] = field(default_factory=dict) starting_stacks: dict[str, int] = field(default_factory=dict) ending_stacks: dict[str, int] = field(default_factory=dict) pot_contributions: list[dict[str, Any]] = field(default_factory=list) showdown_hands: dict[str, list[Card]] = field(default_factory=dict) started_at: float = field(default_factory=time) finished_at: float = field(default_factory=time) def to_dict(self) -> dict[str, object]: return { "game_id": self.game_id, "hand_number": self.hand_number, "button_seat": self.button_seat, "blinds": self.blinds.to_dict() if self.blinds else None, "board": [str(card) for card in self.board], "actions": [record.to_dict() for record in self.actions], "awards": [award.to_dict() for award in self.awards], "hole_cards": { player_id: [str(card) for card in cards] for player_id, cards in self.hole_cards.items() }, "starting_stacks": dict(self.starting_stacks), "ending_stacks": dict(self.ending_stacks), "pot_contributions": [ self._pot_contribution_to_dict(contribution) for contribution in self.pot_contributions ], # ``showdown_hands`` is only populated when more than one player # remained eligible for a pot; empty dict means the hand ended # without a showdown (e.g. everyone folded but the winner). "showdown_hands": { player_id: [str(card) for card in cards] for player_id, cards in self.showdown_hands.items() }, "started_at": self.started_at, "finished_at": self.finished_at, } @staticmethod def _pot_contribution_to_dict(contribution: dict[str, Any]) -> dict[str, object]: hand_value = contribution.get("hand_value") if isinstance(hand_value, HandValue): hand_value = hand_value.to_dict() elif isinstance(hand_value, dict): hand_value = dict(hand_value) raw_contributors = contribution.get("contributors") or {} contributors = { str(player_id): int(amount) for player_id, amount in dict(raw_contributors).items() } return { "amount": int(contribution.get("amount") or 0), "contributors": contributors, "winners": [ str(player_id) for player_id in contribution.get("winners", []) ], "hand_value": hand_value, }