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:
@@ -143,20 +143,22 @@ class GameLogger:
|
||||
)
|
||||
)
|
||||
|
||||
async def log_game_end_async(self, game_id: str) -> None:
|
||||
async def log_game_end_async(
|
||||
self,
|
||||
game_id: str,
|
||||
winner_id: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Mark game as ended.
|
||||
|
||||
Args:
|
||||
game_id: Game UUID.
|
||||
Mark game as ended. winner_id is the player who finished with the
|
||||
lowest total — None when tied or when the caller doesn't have it.
|
||||
"""
|
||||
try:
|
||||
await self.event_store.update_game_completed(game_id)
|
||||
log.debug(f"Logged game end: {game_id}")
|
||||
await self.event_store.update_game_completed(game_id, winner_id)
|
||||
log.debug(f"Logged game end: {game_id} winner={winner_id}")
|
||||
except Exception as e:
|
||||
log.error(f"Failed to log game end: {e}")
|
||||
|
||||
def log_game_end(self, game_id: str) -> None:
|
||||
def log_game_end(self, game_id: str, winner_id: Optional[str] = None) -> None:
|
||||
"""
|
||||
Sync wrapper for log_game_end_async.
|
||||
|
||||
@@ -166,8 +168,8 @@ class GameLogger:
|
||||
return
|
||||
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
asyncio.create_task(self.log_game_end_async(game_id))
|
||||
asyncio.get_running_loop()
|
||||
asyncio.create_task(self.log_game_end_async(game_id, winner_id))
|
||||
except RuntimeError:
|
||||
# Not in async context - skip (simulations don't need this)
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user