fix(server): winner_id on completed games + stats idempotency latch
Two issues in the GAME_OVER broadcast path: 1. log_game_end called update_game_completed with winner_id=None default, so games_v2.winner_id was NULL on all 17 completed staging rows. The denormalized column existed but carried no information. Compute winner (lowest total; None on tie) in broadcast_game_state and thread through. 2. _process_stats_safe had no idempotency guard. log_game_end was already self-guarding via game_log_id=None after first fire, but nothing stopped repeated GAME_OVER broadcasts from re-firing stats and double-counting games_played/games_won. Add Room.stats_processed latch; reset it in handle_start_game so a re-used room still records. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -754,11 +754,24 @@ async def broadcast_game_state(room: Room):
|
||||
# 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:
|
||||
# Determine winner (lowest total; None on tie) so games_v2.winner_id
|
||||
# is actually populated and stats/rating agree with each other.
|
||||
winner_id: Optional[str] = None
|
||||
if room.game.players:
|
||||
lowest = min(p.total_score for p in room.game.players)
|
||||
leaders = [p for p in room.game.players if p.total_score == lowest]
|
||||
if len(leaders) == 1:
|
||||
winner_id = leaders[0].id
|
||||
|
||||
game_logger = get_logger()
|
||||
if game_logger and room.game_log_id:
|
||||
game_logger.log_game_end(room.game_log_id)
|
||||
game_logger.log_game_end(room.game_log_id, winner_id=winner_id)
|
||||
room.game_log_id = None
|
||||
if _stats_service and room.game.players:
|
||||
# Idempotency: latch on room so repeat GAME_OVER broadcasts don't
|
||||
# double-count. Set before scheduling the task — the task itself is
|
||||
# fire-and-forget and might outlive this function.
|
||||
if _stats_service and room.game.players and not room.stats_processed:
|
||||
room.stats_processed = True
|
||||
asyncio.create_task(_process_stats_safe(room))
|
||||
|
||||
for pid, player in list(room.players.items()):
|
||||
|
||||
Reference in New Issue
Block a user