feat: add hand detail API and enrich hand summary fields
- HandSummary: add hole_cards, starting_stacks, ending_stacks, pot_contributions - Engine: capture all players' hole cards (not just showdown), pre/post hand stacks, per-level pot contributions - Server: new GET /game/<game_id>/hands/<hand_number> route - Service: add get_hand_state() method - Tests: add ServerTests for new endpoint, update existing tests - Existing GET /game/<game_id> auto-inherits new fields via shared to_dict()
This commit is contained in:
+56
-5
@@ -62,6 +62,7 @@ class TableGame:
|
||||
self.board = []
|
||||
self.action_history: list[ActionRecord] = []
|
||||
self.hand_summaries: list[HandSummary] = []
|
||||
self._last_pot_contributions: list[dict[str, object]] = []
|
||||
# ``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
|
||||
@@ -116,6 +117,11 @@ class TableGame:
|
||||
|
||||
for player in self.players:
|
||||
player.reset_for_hand()
|
||||
starting_stacks = {
|
||||
player.player_id: player.stack
|
||||
for player in self.players
|
||||
if player.in_hand
|
||||
}
|
||||
|
||||
self._advance_button()
|
||||
assert self.button_index is not None
|
||||
@@ -127,6 +133,11 @@ class TableGame:
|
||||
self._broadcast_game_update()
|
||||
|
||||
self._deal_hole_cards(deck)
|
||||
hole_cards = {
|
||||
player.player_id: list(player.hole_cards)
|
||||
for player in self.players
|
||||
if player.in_hand
|
||||
}
|
||||
small_blind_index, big_blind_index = self._blind_indexes()
|
||||
self._post_blind(small_blind_index, "small_blind", self.small_blind)
|
||||
self._post_blind(big_blind_index, "big_blind", self.big_blind)
|
||||
@@ -150,6 +161,11 @@ class TableGame:
|
||||
self._betting_round(street, start_index, self.big_blind)
|
||||
|
||||
awards = self._award_pots()
|
||||
ending_stacks = {
|
||||
player.player_id: player.stack
|
||||
for player in self.players
|
||||
if player.player_id in starting_stacks
|
||||
}
|
||||
summary = HandSummary(
|
||||
game_id=self.game_id,
|
||||
hand_number=self.hand_number,
|
||||
@@ -158,6 +174,10 @@ class TableGame:
|
||||
actions=list(self.action_history),
|
||||
awards=awards,
|
||||
blinds=active_blinds,
|
||||
hole_cards=hole_cards,
|
||||
starting_stacks=starting_stacks,
|
||||
ending_stacks=ending_stacks,
|
||||
pot_contributions=deepcopy(self._last_pot_contributions),
|
||||
showdown_hands=self._collect_showdown_hands(),
|
||||
started_at=started_at,
|
||||
finished_at=time(),
|
||||
@@ -557,21 +577,43 @@ class TableGame:
|
||||
return current_bet, min_raise, full_raise
|
||||
|
||||
def _award_pots(self) -> list[PotAward]:
|
||||
self._last_pot_contributions = []
|
||||
total_pot = sum(player.total_bet for player in self.players)
|
||||
live_players = [player for player in self.players if self._is_live(player)]
|
||||
if not live_players or total_pot <= 0:
|
||||
return []
|
||||
|
||||
if len(live_players) == 1:
|
||||
live_players[0].stack += total_pot
|
||||
return [PotAward(total_pot, [live_players[0].player_id], None)]
|
||||
|
||||
levels = sorted({player.total_bet for player in self.players if player.total_bet > 0})
|
||||
if len(live_players) == 1:
|
||||
winner = live_players[0]
|
||||
winner.stack += total_pot
|
||||
previous_level = 0
|
||||
for level in levels:
|
||||
contributors = [player for player in self.players if player.total_bet >= level]
|
||||
pot_amount = (level - previous_level) * len(contributors)
|
||||
self._last_pot_contributions.append(
|
||||
{
|
||||
"amount": pot_amount,
|
||||
"contributors": {
|
||||
player.player_id: level - previous_level
|
||||
for player in contributors
|
||||
},
|
||||
"winners": [winner.player_id],
|
||||
"hand_value": None,
|
||||
}
|
||||
)
|
||||
previous_level = level
|
||||
return [PotAward(total_pot, [winner.player_id], None)]
|
||||
|
||||
previous_level = 0
|
||||
awards: list[PotAward] = []
|
||||
for level in levels:
|
||||
contributors = [player for player in self.players if player.total_bet >= level]
|
||||
pot_amount = (level - previous_level) * len(contributors)
|
||||
level_contributions = {
|
||||
player.player_id: level - previous_level
|
||||
for player in contributors
|
||||
}
|
||||
previous_level = level
|
||||
contenders = [player for player in contributors if self._is_live(player)]
|
||||
if not contenders or pot_amount <= 0:
|
||||
@@ -593,13 +635,22 @@ class TableGame:
|
||||
winner.stack += share
|
||||
for winner in ordered_winners[:remainder]:
|
||||
winner.stack += 1
|
||||
winner_ids = [winner.player_id for winner in ordered_winners]
|
||||
awards.append(
|
||||
PotAward(
|
||||
amount=pot_amount,
|
||||
winners=[winner.player_id for winner in ordered_winners],
|
||||
winners=winner_ids,
|
||||
hand_value=best_value,
|
||||
)
|
||||
)
|
||||
self._last_pot_contributions.append(
|
||||
{
|
||||
"amount": pot_amount,
|
||||
"contributors": level_contributions,
|
||||
"winners": winner_ids,
|
||||
"hand_value": best_value,
|
||||
}
|
||||
)
|
||||
return awards
|
||||
|
||||
def _collect_showdown_hands(self) -> dict[str, list]:
|
||||
|
||||
Reference in New Issue
Block a user