204 lines
6.0 KiB
Python
204 lines
6.0 KiB
Python
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
|
|
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],
|
|
# ``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,
|
|
}
|