fix: game service api block when a game is running
This commit is contained in:
+63
-18
@@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from random import Random
|
||||
from threading import RLock
|
||||
from time import time
|
||||
|
||||
from texas_holdem.agents import PokerAgent
|
||||
@@ -54,6 +56,7 @@ class TableGame:
|
||||
self.small_blind = small_blind
|
||||
self.big_blind = big_blind
|
||||
self.rng = rng or Random()
|
||||
self.lock = RLock()
|
||||
self.hand_number = 0
|
||||
self.button_index: int | None = None
|
||||
self.board = []
|
||||
@@ -64,6 +67,7 @@ class TableGame:
|
||||
# first hand that played under those stakes, which makes it trivial
|
||||
# to reconstruct the schedule from the outside.
|
||||
self.blind_history: list[BlindLevel] = []
|
||||
self._completed_snapshot: dict[str, object] = self._to_dict_unlocked()
|
||||
|
||||
@property
|
||||
def is_complete(self) -> bool:
|
||||
@@ -83,6 +87,14 @@ class TableGame:
|
||||
:attr:`blind_history` whenever it changes (including the very first
|
||||
hand) so external observers can replay the schedule.
|
||||
"""
|
||||
with self.lock:
|
||||
return self._run_hand_locked(small_blind=small_blind, big_blind=big_blind)
|
||||
|
||||
def _run_hand_locked(
|
||||
self,
|
||||
small_blind: int | None = None,
|
||||
big_blind: int | None = None,
|
||||
) -> HandSummary:
|
||||
if self.is_complete:
|
||||
raise GameComplete("game is complete")
|
||||
|
||||
@@ -151,6 +163,7 @@ class TableGame:
|
||||
finished_at=time(),
|
||||
)
|
||||
self.hand_summaries.append(summary)
|
||||
self._completed_snapshot = deepcopy(self._to_dict_unlocked())
|
||||
return summary
|
||||
|
||||
def run_hands(
|
||||
@@ -166,24 +179,47 @@ class TableGame:
|
||||
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
|
||||
# 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
|
||||
with self.lock:
|
||||
if max_hands <= 0:
|
||||
raise ValueError("max_hands must be positive")
|
||||
summaries = []
|
||||
for _ in range(max_hands):
|
||||
if self.is_complete:
|
||||
break
|
||||
# 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_locked(
|
||||
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
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
with self.lock:
|
||||
return self._to_dict_unlocked()
|
||||
|
||||
def snapshot_completed(self) -> dict[str, object]:
|
||||
"""Return a stable snapshot from the latest completed hand boundary.
|
||||
|
||||
If a hand is currently running under ``self.lock``, this method does
|
||||
not block. It returns the most recent completed hand summary and
|
||||
stacks captured in memory, which is exactly what status endpoints
|
||||
need while a long-running HTTP-agent decision is in progress.
|
||||
"""
|
||||
if self.lock.acquire(blocking=False):
|
||||
try:
|
||||
return deepcopy(self._to_dict_unlocked())
|
||||
finally:
|
||||
self.lock.release()
|
||||
return deepcopy(self._completed_snapshot)
|
||||
|
||||
def _to_dict_unlocked(self) -> dict[str, object]:
|
||||
return {
|
||||
"game_id": self.game_id,
|
||||
"status": "complete" if self.is_complete else "running",
|
||||
@@ -449,9 +485,18 @@ class TableGame:
|
||||
try:
|
||||
requested = agent.decide(observation)
|
||||
except Exception:
|
||||
requested = PlayerAction("fold")
|
||||
requested = self._default_action(observation.legal_actions)
|
||||
return self._coerce_action(requested, observation.legal_actions)
|
||||
|
||||
def _default_action(self, legal_actions: list[dict[str, object]]) -> PlayerAction:
|
||||
by_action = {str(action["action"]): action for action in legal_actions}
|
||||
for action_type in ("check", "call", "fold"):
|
||||
if action_type in by_action:
|
||||
legal = by_action[action_type]
|
||||
return PlayerAction(action_type, int(legal.get("amount") or 0))
|
||||
legal = legal_actions[0]
|
||||
return PlayerAction(str(legal["action"]), int(legal.get("amount") or 0))
|
||||
|
||||
def _coerce_action(
|
||||
self,
|
||||
requested: PlayerAction,
|
||||
@@ -581,7 +626,7 @@ class TableGame:
|
||||
swallow individual exceptions so a flaky remote endpoint cannot
|
||||
break the table flow.
|
||||
"""
|
||||
snapshot = self.to_dict()
|
||||
snapshot = self._to_dict_unlocked()
|
||||
for agent in self.agents.values():
|
||||
try:
|
||||
agent.on_game_update(snapshot)
|
||||
|
||||
Reference in New Issue
Block a user