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 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] 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, "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], # ``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, }