Cancel CPU turns immediately when host ends game
Convert CPU turn chain to a cancellable asyncio.Task tracked on Room, so ending the game or leaving no longer blocks waiting for CPU sleeps. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4c23f2b4a9
commit
de3495635b
@ -1934,7 +1934,11 @@ def _log_cpu_action(logger, game_id: Optional[str], cpu_player: Player, game: Ga
|
||||
async def process_cpu_turn(
|
||||
game: Game, cpu_player: Player, broadcast_callback, game_id: Optional[str] = None
|
||||
) -> None:
|
||||
"""Process a complete turn for a CPU player."""
|
||||
"""Process a complete turn for a CPU player.
|
||||
|
||||
May raise asyncio.CancelledError if the game is ended mid-turn.
|
||||
The caller (check_and_run_cpu_turn) handles cancellation.
|
||||
"""
|
||||
import asyncio
|
||||
from services.game_logger import get_logger
|
||||
|
||||
|
||||
@ -473,6 +473,15 @@ async def handle_end_game(data: dict, ctx: ConnectionContext, *, room_manager, c
|
||||
await ctx.websocket.send_json({"type": "error", "message": "Only the host can end the game"})
|
||||
return
|
||||
|
||||
# Cancel any running CPU turn task so the game ends immediately
|
||||
if ctx.current_room.cpu_turn_task:
|
||||
ctx.current_room.cpu_turn_task.cancel()
|
||||
try:
|
||||
await ctx.current_room.cpu_turn_task
|
||||
except (asyncio.CancelledError, Exception):
|
||||
pass
|
||||
ctx.current_room.cpu_turn_task = None
|
||||
|
||||
await ctx.current_room.broadcast({
|
||||
"type": "game_ended",
|
||||
"reason": "Host ended the game",
|
||||
|
||||
@ -706,7 +706,29 @@ async def broadcast_game_state(room: Room):
|
||||
|
||||
|
||||
async def check_and_run_cpu_turn(room: Room):
|
||||
"""Check if current player is CPU and run their turn."""
|
||||
"""Check if current player is CPU and run their turn.
|
||||
|
||||
If this is the outermost call (no CPU task running), creates a single
|
||||
asyncio.Task that runs the entire chain of consecutive CPU turns.
|
||||
This allows the task to be cancelled cleanly when the game ends.
|
||||
"""
|
||||
# If already inside a CPU turn task, run directly (no nested tasks)
|
||||
if room.cpu_turn_task is not None:
|
||||
await _run_cpu_chain(room)
|
||||
return
|
||||
|
||||
# Outermost call: wrap the chain in a cancellable task
|
||||
room.cpu_turn_task = asyncio.create_task(_run_cpu_chain(room))
|
||||
try:
|
||||
await room.cpu_turn_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
finally:
|
||||
room.cpu_turn_task = None
|
||||
|
||||
|
||||
async def _run_cpu_chain(room: Room):
|
||||
"""Run consecutive CPU turns until a human player's turn or game ends."""
|
||||
if room.game.phase not in (GamePhase.PLAYING, GamePhase.FINAL_TURN):
|
||||
return
|
||||
|
||||
@ -728,11 +750,20 @@ async def check_and_run_cpu_turn(room: Room):
|
||||
await process_cpu_turn(room.game, current, broadcast_cb, game_id=room.game_log_id)
|
||||
|
||||
# Check if next player is also CPU (chain CPU turns)
|
||||
await check_and_run_cpu_turn(room)
|
||||
await _run_cpu_chain(room)
|
||||
|
||||
|
||||
async def handle_player_leave(room: Room, player_id: str):
|
||||
"""Handle a player leaving a room."""
|
||||
# Cancel any running CPU turn task before cleanup
|
||||
if room.cpu_turn_task:
|
||||
room.cpu_turn_task.cancel()
|
||||
try:
|
||||
await room.cpu_turn_task
|
||||
except (asyncio.CancelledError, Exception):
|
||||
pass
|
||||
room.cpu_turn_task = None
|
||||
|
||||
room_code = room.code
|
||||
room_player = room.remove_player(player_id)
|
||||
|
||||
|
||||
@ -69,6 +69,7 @@ class Room:
|
||||
settings: dict = field(default_factory=lambda: {"decks": 1, "rounds": 1})
|
||||
game_log_id: Optional[str] = None
|
||||
game_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
|
||||
cpu_turn_task: Optional[asyncio.Task] = None
|
||||
|
||||
def add_player(
|
||||
self,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user