feat: set blind bet by run hands
This commit is contained in:
+102
-3
@@ -8,6 +8,7 @@ from texas_holdem.cards import Deck
|
||||
from texas_holdem.evaluator import evaluate
|
||||
from texas_holdem.models import (
|
||||
ActionRecord,
|
||||
BlindLevel,
|
||||
HandSummary,
|
||||
Observation,
|
||||
PlayerAction,
|
||||
@@ -58,16 +59,44 @@ class TableGame:
|
||||
self.board = []
|
||||
self.action_history: list[ActionRecord] = []
|
||||
self.hand_summaries: list[HandSummary] = []
|
||||
# ``blind_history`` is an append-only log of every blind level change
|
||||
# (including the initial one). Each entry's ``hand_number`` is the
|
||||
# first hand that played under those stakes, which makes it trivial
|
||||
# to reconstruct the schedule from the outside.
|
||||
self.blind_history: list[BlindLevel] = []
|
||||
|
||||
@property
|
||||
def is_complete(self) -> bool:
|
||||
return len([player for player in self.players if player.stack > 0]) < 2
|
||||
|
||||
def run_hand(self) -> HandSummary:
|
||||
def run_hand(
|
||||
self,
|
||||
small_blind: int | None = None,
|
||||
big_blind: int | None = None,
|
||||
) -> HandSummary:
|
||||
"""Play a single hand.
|
||||
|
||||
``small_blind`` / ``big_blind`` allow callers to bump the stakes
|
||||
between hands without rebuilding the table. Either both must be
|
||||
provided or both omitted (in which case the previously configured
|
||||
blinds carry over). The resolved blind level is appended to
|
||||
:attr:`blind_history` whenever it changes (including the very first
|
||||
hand) so external observers can replay the schedule.
|
||||
"""
|
||||
if self.is_complete:
|
||||
raise GameComplete("game is complete")
|
||||
|
||||
self._apply_blinds_for_hand(small_blind, big_blind)
|
||||
|
||||
self.hand_number += 1
|
||||
# Stamp the active blind level onto the upcoming summary so a hand
|
||||
# remains self-describing even after the blinds change later on.
|
||||
active_blinds = BlindLevel(
|
||||
hand_number=self.hand_number,
|
||||
small_blind=self.small_blind,
|
||||
big_blind=self.big_blind,
|
||||
)
|
||||
self._record_blind_level_if_new(active_blinds)
|
||||
started_at = time()
|
||||
self.board = []
|
||||
self.action_history = []
|
||||
@@ -116,6 +145,7 @@ class TableGame:
|
||||
board=list(self.board),
|
||||
actions=list(self.action_history),
|
||||
awards=awards,
|
||||
blinds=active_blinds,
|
||||
showdown_hands=self._collect_showdown_hands(),
|
||||
started_at=started_at,
|
||||
finished_at=time(),
|
||||
@@ -123,14 +153,32 @@ class TableGame:
|
||||
self.hand_summaries.append(summary)
|
||||
return summary
|
||||
|
||||
def run_hands(self, max_hands: int, until_one_left: bool = False) -> list[HandSummary]:
|
||||
def run_hands(
|
||||
self,
|
||||
max_hands: int,
|
||||
until_one_left: bool = False,
|
||||
small_blind: int | None = None,
|
||||
big_blind: int | None = None,
|
||||
) -> list[HandSummary]:
|
||||
"""Play up to ``max_hands`` hands using a single blind configuration.
|
||||
|
||||
Passing ``small_blind`` / ``big_blind`` bumps the stakes starting
|
||||
with the first hand of this call; subsequent calls can raise them
|
||||
again. Leaving them ``None`` keeps the current level unchanged.
|
||||
"""
|
||||
if max_hands <= 0:
|
||||
raise ValueError("max_hands must be positive")
|
||||
summaries = []
|
||||
for _ in range(max_hands):
|
||||
if self.is_complete:
|
||||
break
|
||||
summaries.append(self.run_hand())
|
||||
# Only the first hand of the batch needs to apply the blind
|
||||
# override; after that the engine reuses the stored values.
|
||||
summaries.append(
|
||||
self.run_hand(small_blind=small_blind, big_blind=big_blind)
|
||||
)
|
||||
small_blind = None
|
||||
big_blind = None
|
||||
if until_one_left and self.is_complete:
|
||||
break
|
||||
return summaries
|
||||
@@ -143,8 +191,18 @@ class TableGame:
|
||||
"button_seat": None
|
||||
if self.button_index is None
|
||||
else self.players[self.button_index].seat,
|
||||
# ``small_blind`` / ``big_blind`` mirror the *current* level so
|
||||
# legacy callers keep working. New consumers should prefer the
|
||||
# structured ``blinds`` block which carries the full schedule.
|
||||
"small_blind": self.small_blind,
|
||||
"big_blind": self.big_blind,
|
||||
"blinds": {
|
||||
"current": {
|
||||
"small_blind": self.small_blind,
|
||||
"big_blind": self.big_blind,
|
||||
},
|
||||
"history": [level.to_dict() for level in self.blind_history],
|
||||
},
|
||||
"starting_stack": self.starting_stack,
|
||||
"players": [player.public_dict() for player in self.players],
|
||||
# ``hands`` exposes every finished hand (each entry is the same
|
||||
@@ -153,6 +211,47 @@ class TableGame:
|
||||
"hands": [summary.to_dict() for summary in self.hand_summaries],
|
||||
}
|
||||
|
||||
def _apply_blinds_for_hand(
|
||||
self,
|
||||
small_blind: int | None,
|
||||
big_blind: int | None,
|
||||
) -> None:
|
||||
"""Validate and apply optional per-hand blind overrides.
|
||||
|
||||
Splitting this out keeps :meth:`run_hand` focused on the table flow
|
||||
while letting us reuse the validation rules originally enforced by
|
||||
``__init__``. We require both values to be supplied together so the
|
||||
configuration cannot drift into an inconsistent half-update.
|
||||
"""
|
||||
if small_blind is None and big_blind is None:
|
||||
return
|
||||
if small_blind is None or big_blind is None:
|
||||
raise ValueError(
|
||||
"small_blind and big_blind must be provided together"
|
||||
)
|
||||
if small_blind <= 0 or big_blind <= 0 or small_blind > big_blind:
|
||||
raise ValueError("blinds must satisfy 0 < small_blind <= big_blind")
|
||||
self.small_blind = int(small_blind)
|
||||
self.big_blind = int(big_blind)
|
||||
|
||||
def _record_blind_level_if_new(self, level: BlindLevel) -> None:
|
||||
"""Append ``level`` to :attr:`blind_history` when it differs.
|
||||
|
||||
Comparing against the latest entry (rather than blindly appending)
|
||||
keeps the log compact: stretches of unchanged stakes only contribute
|
||||
a single record. The very first hand always seeds an entry because
|
||||
the history starts empty.
|
||||
"""
|
||||
if not self.blind_history:
|
||||
self.blind_history.append(level)
|
||||
return
|
||||
latest = self.blind_history[-1]
|
||||
if (
|
||||
latest.small_blind != level.small_blind
|
||||
or latest.big_blind != level.big_blind
|
||||
):
|
||||
self.blind_history.append(level)
|
||||
|
||||
def _advance_button(self) -> None:
|
||||
if self.button_index is None:
|
||||
self.button_index = self._next_index(0, lambda index: self.players[index].stack > 0)
|
||||
|
||||
Reference in New Issue
Block a user