fix(server): game completion pipeline — stats recording + dict iteration safety

Three bugs prevented game stats from recording:

1. broadcast_game_state had game_over processing (log_game_end + stats)
   inside the per-player loop — if all players disconnected before the
   loop ran, stats never processed. Moved to run once before the loop.

2. room.broadcast and broadcast_game_state iterated players.items()
   without snapshotting, causing RuntimeError when concurrent player
   disconnects mutated the dict. Fixed with list().

3. stats_service.process_game_from_state passed avg_round_score to a
   CASE expression without a type hint, causing asyncpg to fail with
   "could not determine data type of parameter $6". Added ::integer
   casts.

Also wrapped per-player send_json calls in try/except so a single
disconnected player doesn't abort the broadcast to remaining players.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-17 20:37:08 -04:00
parent d5194f43ba
commit ccc2f3b559
3 changed files with 49 additions and 42 deletions

View File

@@ -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,