diff --git a/server/main.py b/server/main.py index 835c3c1..afddb93 100644 --- a/server/main.py +++ b/server/main.py @@ -751,16 +751,29 @@ async def broadcast_game_state(room: Room): spectator_state = room.game.get_state(None) # No player perspective await _spectator_manager.send_game_state(room.code, spectator_state) - for pid, player in room.players.items(): + # Process game completion BEFORE the per-player loop so it runs exactly + # once and isn't gated on any player still being connected. + if room.game.phase == GamePhase.GAME_OVER: + game_logger = get_logger() + if game_logger and room.game_log_id: + game_logger.log_game_end(room.game_log_id) + room.game_log_id = None + if _stats_service and room.game.players: + asyncio.create_task(_process_stats_safe(room)) + + for pid, player in list(room.players.items()): # Skip CPU players if player.is_cpu or not player.websocket: continue game_state = room.game.get_state(pid) - await player.websocket.send_json({ - "type": "game_state", - "game_state": game_state, - }) + try: + await player.websocket.send_json({ + "type": "game_state", + "game_state": game_state, + }) + except Exception: + continue # Check for round over if room.game.phase == GamePhase.ROUND_OVER: @@ -768,47 +781,41 @@ async def broadcast_game_state(room: Room): {"id": p.id, "name": p.name, "score": p.score, "total": p.total_score, "rounds_won": p.rounds_won} for p in room.game.players ] - # Build rankings by_points = sorted(scores, key=lambda x: x["total"]) by_holes_won = sorted(scores, key=lambda x: -x["rounds_won"]) - await player.websocket.send_json({ - "type": "round_over", - "scores": scores, - "finisher_id": room.game.finisher_id, - "round": room.game.current_round, - "total_rounds": room.game.num_rounds, - "rankings": { - "by_points": by_points, - "by_holes_won": by_holes_won, - }, - }) + try: + await player.websocket.send_json({ + "type": "round_over", + "scores": scores, + "finisher_id": room.game.finisher_id, + "round": room.game.current_round, + "total_rounds": room.game.num_rounds, + "rankings": { + "by_points": by_points, + "by_holes_won": by_holes_won, + }, + }) + except Exception: + pass - # Check for game over elif room.game.phase == GamePhase.GAME_OVER: - # Log game end - game_logger = get_logger() - if game_logger and room.game_log_id: - game_logger.log_game_end(room.game_log_id) - room.game_log_id = None # Clear to avoid duplicate logging - - # Process stats asynchronously (fire-and-forget) to avoid delaying game over notifications - if _stats_service and room.game.players: - asyncio.create_task(_process_stats_safe(room)) - scores = [ {"name": p.name, "total": p.total_score, "rounds_won": p.rounds_won} for p in room.game.players ] by_points = sorted(scores, key=lambda x: x["total"]) by_holes_won = sorted(scores, key=lambda x: -x["rounds_won"]) - await player.websocket.send_json({ - "type": "game_over", - "final_scores": by_points, - "rankings": { - "by_points": by_points, - "by_holes_won": by_holes_won, - }, - }) + try: + await player.websocket.send_json({ + "type": "game_over", + "final_scores": by_points, + "rankings": { + "by_points": by_points, + "by_holes_won": by_holes_won, + }, + }) + except Exception: + pass # Notify current player it's their turn (only if human) elif room.game.phase in (GamePhase.PLAYING, GamePhase.FINAL_TURN): diff --git a/server/room.py b/server/room.py index c698f85..0bbb95d 100644 --- a/server/room.py +++ b/server/room.py @@ -232,7 +232,7 @@ class Room: message: JSON-serializable message dict. exclude: Optional player ID to skip. """ - for player_id, player in self.players.items(): + for player_id, player in list(self.players.items()): if player_id != exclude and player.websocket and not player.is_cpu: try: await player.websocket.send_json(message) diff --git a/server/services/stats_service.py b/server/services/stats_service.py index cedda22..bc19e6c 100644 --- a/server/services/stats_service.py +++ b/server/services/stats_service.py @@ -781,7 +781,7 @@ class StatsService: # We don't have per-round data in legacy mode, so some stats are limited # Use total_score / num_rounds as an approximation for avg round score - avg_round_score = total_score / num_rounds if num_rounds > 0 else None + avg_round_score = total_score // num_rounds if num_rounds > 0 else total_score # Update stats await conn.execute(""" @@ -792,13 +792,13 @@ class StatsService: rounds_won = rounds_won + $4, total_points = total_points + $5, best_score = CASE - WHEN best_score IS NULL THEN $6 - WHEN $6 IS NOT NULL AND $6 < best_score THEN $6 + WHEN best_score IS NULL THEN $6::integer + WHEN $6::integer IS NOT NULL AND $6::integer < best_score THEN $6::integer ELSE best_score END, worst_score = CASE - WHEN worst_score IS NULL THEN $7 - WHEN $7 IS NOT NULL AND $7 > worst_score THEN $7 + WHEN worst_score IS NULL THEN $7::integer + WHEN $7::integer IS NOT NULL AND $7::integer > worst_score THEN $7::integer ELSE worst_score END, current_win_streak = CASE WHEN $2 = 1 THEN current_win_streak + 1 ELSE 0 END,