2626if TYPE_CHECKING :
2727 from litestar import Router , WebSocket
2828
29+ from scribbl_py .auth .db_service import DatabaseAuthService
2930 from scribbl_py .game .models import GameRoom
3031 from scribbl_py .services .game import GameService
3132
@@ -41,6 +42,7 @@ class GameConnection:
4142 player_id : UUID
4243 user_id : str
4344 user_name : str
45+ auth_user_id : UUID | None = None # Linked auth user for stats
4446 connected_at : datetime = field (default_factory = lambda : datetime .now (UTC ))
4547
4648
@@ -100,20 +102,32 @@ def __init__(
100102 self ,
101103 game_service : GameService ,
102104 connection_manager : ConnectionManager | None = None ,
105+ auth_service : DatabaseAuthService | None = None ,
103106 ) -> None :
104107 """Initialize the game WebSocket handler.
105108
106109 Args:
107110 game_service: The game service instance.
108111 connection_manager: Optional connection manager for advanced tracking.
112+ auth_service: Optional auth service for user stats tracking.
109113 """
110114 self ._service = game_service
111115 self ._manager = connection_manager or ConnectionManager ()
116+ self ._auth_service = auth_service
117+ self ._plugin : Any = None # Set by plugin for lazy auth service access
112118 self ._connections : dict [int , GameConnection ] = {} # socket_id -> connection
113119 self ._room_sockets : dict [UUID , set [int ]] = {} # room_id -> socket_ids
114120 self ._timer_tasks : dict [UUID , asyncio .Task ] = {} # room_id -> timer task
115121 self ._lobby_browsers : dict [int , WebSocket ] = {} # socket_id -> socket for lobby browsers
116122
123+ def _get_auth_service (self ) -> DatabaseAuthService | None :
124+ """Get auth service, trying plugin reference if direct reference is None."""
125+ if self ._auth_service is not None :
126+ return self ._auth_service
127+ if self ._plugin is not None and hasattr (self ._plugin , "_auth_service" ):
128+ return self ._plugin ._auth_service
129+ return None
130+
117131 async def handle_lobby_connection (self , socket : WebSocket , room_id : UUID ) -> None :
118132 """Handle WebSocket connection to game lobby.
119133
@@ -336,13 +350,18 @@ async def _handle_join(
336350 socket: The WebSocket connection.
337351 socket_id: Socket identifier.
338352 room_id: The room ID.
339- data: Message data with user_id and user_name .
353+ data: Message data with user_id, user_name, and optional auth_user_id .
340354 """
341355 user_id = data .get ("user_id" , "" )
342356 user_name = data .get ("user_name" , "Anonymous" )
357+ auth_user_id_str = data .get ("auth_user_id" )
358+ auth_user_id = UUID (auth_user_id_str ) if auth_user_id_str else None
343359
344360 try :
345361 player = self ._service .join_room (room_id , user_id , user_name )
362+ # Store auth user ID on player for stats tracking
363+ if auth_user_id :
364+ player .auth_user_id = auth_user_id
346365 except (GameNotFoundError , GameStateError ) as e :
347366 await self ._send_error (socket , "join_failed" , str (e ))
348367 return
@@ -354,6 +373,7 @@ async def _handle_join(
354373 player_id = player .id ,
355374 user_id = user_id ,
356375 user_name = user_name ,
376+ auth_user_id = auth_user_id ,
357377 )
358378 self ._connections [socket_id ] = connection
359379
@@ -1513,6 +1533,11 @@ async def _end_round(self, room_id: UUID) -> None:
15131533 ],
15141534 },
15151535 )
1536+
1537+ # Record game stats for authenticated players
1538+ auth_svc = self ._get_auth_service ()
1539+ if auth_svc :
1540+ await self ._record_game_stats (room , results , auth_svc )
15161541 else :
15171542 # Start next round after delay
15181543 await asyncio .sleep (5 )
@@ -1523,6 +1548,59 @@ async def _end_round(self, room_id: UUID) -> None:
15231548 except Exception :
15241549 pass
15251550
1551+ async def _record_game_stats (
1552+ self ,
1553+ room : GameRoom ,
1554+ results : dict [str , Any ],
1555+ auth_service : DatabaseAuthService ,
1556+ ) -> None :
1557+ """Record game stats for authenticated players.
1558+
1559+ Args:
1560+ room: The game room.
1561+ results: Game results from end_round.
1562+ auth_service: Auth service for recording stats.
1563+ """
1564+
1565+ leaderboard = results .get ("leaderboard" , [])
1566+ if not leaderboard :
1567+ return
1568+
1569+ # Winner is first in leaderboard
1570+ winner_player = leaderboard [0 ][0 ] if leaderboard else None
1571+
1572+ for player , score in leaderboard :
1573+ # Skip players without auth user ID
1574+ if not player .auth_user_id :
1575+ continue
1576+
1577+ won = player .id == winner_player .id if winner_player else False
1578+
1579+ try :
1580+ await auth_service .record_game_result (
1581+ user_id = player .auth_user_id ,
1582+ score = score ,
1583+ won = won ,
1584+ correct_guesses = 1 if player .has_guessed else 0 , # Simplified
1585+ total_guesses = 1 , # Simplified
1586+ total_guess_time_ms = int ((player .guess_time or 0 ) * 1000 ),
1587+ fastest_guess_ms = int ((player .guess_time or 0 ) * 1000 ) if player .guess_time else None ,
1588+ drawings_completed = 1 , # Simplified - each player drew once per round
1589+ drawings_guessed = 1 if player .has_guessed else 0 ,
1590+ )
1591+ logger .info (
1592+ "Recorded game stats" ,
1593+ user_id = str (player .auth_user_id ),
1594+ score = score ,
1595+ won = won ,
1596+ )
1597+ except Exception as e :
1598+ logger .error (
1599+ "Failed to record game stats" ,
1600+ user_id = str (player .auth_user_id ),
1601+ error = str (e ),
1602+ )
1603+
15261604 async def _broadcast_round_started (
15271605 self ,
15281606 room_id : UUID ,
@@ -1684,20 +1762,22 @@ def create_game_websocket_handler(
16841762 path : str ,
16851763 game_service : GameService ,
16861764 connection_manager : ConnectionManager | None = None ,
1765+ auth_service : DatabaseAuthService | None = None ,
16871766) -> tuple [Router , GameWebSocketHandler ]:
16881767 """Create a WebSocket router for game real-time communication.
16891768
16901769 Args:
16911770 path: Base path for WebSocket routes.
16921771 game_service: The game service instance.
16931772 connection_manager: Optional connection manager.
1773+ auth_service: Optional auth service for stats tracking.
16941774
16951775 Returns:
16961776 A tuple of (Litestar Router, GameWebSocketHandler instance).
16971777 """
16981778 from litestar import Router , websocket
16991779
1700- handler = GameWebSocketHandler (game_service , connection_manager )
1780+ handler = GameWebSocketHandler (game_service , connection_manager , auth_service )
17011781
17021782 @websocket (path = "/lobby/{room_id:uuid}" )
17031783 async def lobby_websocket (socket : WebSocket , room_id : UUID ) -> None :
0 commit comments