11import asyncio
22import time
33import traceback
4- from typing import Optional
54
6- from api .endpoints .users import get_current_user , get_current_user_ws
5+ from typing import List , Tuple
6+ from fastapi import APIRouter , Depends , WebSocket , WebSocketDisconnect
7+ from sqlalchemy .orm import Session
8+
9+ from api .endpoints .users .websockets import get_current_user_ws
710from core .config import settings
11+ from core .errors .game import SubmittingTooFastError
812from db .models .user import User
913from db .session import get_db
10- from fastapi import APIRouter , Depends , WebSocket , WebSocketDisconnect
11- from schemas .game import GameEvent , GameView
14+ from schemas .game import GameEvent
1215from services .execution .service import code_execution
1316from services .game .ability import ability_manager
1417from services .game .manager import game_manager
15- from services .game .state import GameStatus
18+ from services .game .state import GameStatus , GameState
1619from services .problem .service import ProblemManager
17- from sqlalchemy .orm import Session
1820
1921router = APIRouter (prefix = "/game" , tags = ["game" ])
2022matchmaker = game_manager .matchmaker
@@ -42,52 +44,8 @@ async def queue_websocket(
4244 if not await matchmaker .add_to_queue (websocket , current_user , ranked = False ):
4345 return await websocket .close (code = 4000 , reason = "Already in queue" )
4446
45- while True :
46- try :
47- # Since manual disconnection doesn't fire the event, we have to manually check for disconnection
48- await asyncio .wait_for (websocket .receive_text (), timeout = 1.0 )
49- continue
50- # Timeout means the WebSocket is still connected
51- except asyncio .TimeoutError :
52- # Try to find a match
53- match = await matchmaker .get_random_player (2 )
54-
55- if len (match ) > 0 :
56- ws1 , player1 = match [0 ]
57- ws2 , player2 = match [1 ]
58-
59- # Get problems for the match
60- distribution = matchmaker .get_problem_distribution ()
61- problems = await ProblemManager .get_problems_by_distribution (
62- db , distribution
63- )
64-
65- # Create game and notify players
66- game_state = await game_manager .create_game (
67- player1 , player2 , problems , "unranked" , db
68- )
69-
70- match_data = {
71- "match_id" : game_state .id ,
72- "opponent" : {
73- "username" : player2 .username ,
74- "display_name" : player2 .display_name ,
75- },
76- }
77- await ws1 .send_json ({"type" : "match_found" , "data" : match_data })
78-
79- match_data ["opponent" ] = {
80- "username" : player1 .username ,
81- "display_name" : player1 .display_name ,
82- }
83-
84- await ws2 .send_json ({"type" : "match_found" , "data" : match_data })
47+ await _process_matchmaking_queue (websocket , current_user , db , ranked = False )
8548
86- break
87-
88- await asyncio .sleep (1 )
89-
90- # Remove the user from the queue if they disconnect or some other error occurs
9149 except WebSocketDisconnect :
9250 await matchmaker .remove_from_queue (current_user .id )
9351 except Exception as e :
@@ -102,77 +60,102 @@ async def ranked_queue_websocket(
10260 current_user : User = Depends (get_current_user_ws ),
10361 db : Session = Depends (get_db ),
10462):
105- """
106- WebSocket endpoint for the ranked matchmaking queue
107-
108- :param websocket: WebSocket object
109- :param current_user: User object
110- :param db: Database session
111- """
112-
63+ """Handle ranked matchmaking queue connections."""
11364 try :
114- # Check if the user is already in a game
65+ # Check existing game and queue status
11566 if game_manager .get_player_game (current_user .id ):
11667 return await websocket .close (code = 4000 , reason = "Already in a game" )
11768
118- # Only add the user to the queue if they're not already in it
11969 if not await matchmaker .add_to_queue (websocket , current_user , ranked = True ):
12070 return await websocket .close (code = 4000 , reason = "Already in queue" )
12171
122- while True :
123- try :
124- await asyncio .wait_for (websocket .receive_text (), timeout = 1.0 )
125- continue
126- except asyncio .TimeoutError :
127- # Try to find a ranked match
128- match = await matchmaker .get_ranked_match ()
72+ await _process_matchmaking_queue (websocket , current_user , db , ranked = True )
73+
74+ except WebSocketDisconnect :
75+ await matchmaker .remove_from_queue (current_user .id )
76+ except Exception as e :
77+ print (traceback .format_exc ())
78+ print (f"Error in ranked queue websocket: { e } " )
79+ await matchmaker .remove_from_queue (current_user .id )
12980
81+
82+ async def _process_matchmaking_queue (websocket , current_user , db , ranked = False ):
83+ """
84+ Process matchmaking queue for user
85+
86+ :param websocket: WebSocket connection
87+ :param current_user: Current user
88+ :param db: Database session
89+ :param ranked: Boolean indicating ranked or unranked match
90+ """
91+ while True :
92+ try :
93+ # Manual disconnection doesn't fire the event, so we keep checking for it
94+ await asyncio .wait_for (websocket .receive_text (), timeout = 1.0 )
95+ continue
96+ except asyncio .TimeoutError :
97+ if ranked :
98+ match = await matchmaker .get_ranked_match ()
13099 if match :
131- ws1 , player1 = match [0 ]
132- ws2 , player2 = match [1 ]
100+ await _setup_ranked_match (match , db )
101+ break
102+ else :
103+ match = await matchmaker .get_random_player (2 )
104+ if len (match ) > 0 :
105+ await _setup_unranked_match (match , db )
106+ break
133107
134- # Get problems based on average rating
135- avg_rating = (player1 .rating + player2 .rating ) / 2
136- distribution = matchmaker .get_problem_distribution (
137- ranked = True , rating = avg_rating
138- )
108+ await asyncio .sleep (1 )
139109
140- problems = await ProblemManager .get_problems_by_distribution (
141- db , distribution
142- )
143110
144- # Create game and notify players
145- game_state = await game_manager .create_game (
146- player1 , player2 , problems , "ranked" , db
147- )
111+ async def _setup_unranked_match (match : List [Tuple [WebSocket , User ]], db : Session ):
112+ player1 = match [0 ][1 ]
113+ player2 = match [1 ][1 ]
148114
149- match_data = {
150- "match_id" : game_state .id ,
151- "opponent" : {
152- "username" : player2 .username ,
153- "display_name" : player2 .display_name ,
154- },
155- }
115+ # Fetch problems
116+ distribution = matchmaker .get_problem_distribution ()
117+ problems = await ProblemManager .get_problems_by_distribution (db , distribution )
156118
157- await ws1 .send_json ({"type" : "match_found" , "data" : match_data })
119+ # Create game and notify players
120+ game = await game_manager .create_game (player1 , player2 , problems , "unranked" , db )
121+ await _notify_match_found (match , game .id )
158122
159- match_data ["opponent" ] = {
160- "username" : player1 .username ,
161- "display_name" : player1 .display_name ,
162- }
163123
164- await ws2 .send_json ({"type" : "match_found" , "data" : match_data })
124+ async def _setup_ranked_match (match : List [Tuple [WebSocket , User ]], db : Session ):
125+ player1 = match [0 ][1 ]
126+ player2 = match [1 ][1 ]
165127
166- break
128+ # Fetch problems based on players' ratings
129+ distribution = matchmaker .get_problem_distribution (
130+ ranked = True , rating1 = player1 .rating , rating2 = player2 .rating
131+ )
132+ problems = await ProblemManager .get_problems_by_distribution (db , distribution )
167133
168- await asyncio .sleep (1 )
134+ # Create game and notify players
135+ game = await game_manager .create_game (player1 , player2 , problems , "ranked" , db )
136+ await _notify_match_found (match , game .id )
169137
170- except WebSocketDisconnect :
171- await matchmaker .remove_from_queue (current_user .id )
172- except Exception as e :
173- print (traceback .format_exc ())
174- print (f"Error in ranked queue websocket: { e } " )
175- await matchmaker .remove_from_queue (current_user .id )
138+
139+ async def _notify_match_found (match : List [Tuple [WebSocket , User ]], match_id : str ):
140+ ws1 , player1 = match [0 ]
141+ ws2 , player2 = match [1 ]
142+
143+ match_data = {
144+ "match_id" : match_id ,
145+ "opponent" : {
146+ "username" : player2 .username ,
147+ "display_name" : player2 .display_name ,
148+ },
149+ }
150+
151+ await ws1 .send_json ({"type" : "match_found" , "data" : match_data })
152+
153+ match_data ["opponent" ] = {
154+ "username" : player1 .username ,
155+ "display_name" : player1 .display_name ,
156+ }
157+
158+ await ws2 .send_json ({"type" : "match_found" , "data" : match_data })
176159
177160
178161@router .websocket ("/play/{game_id}" )
@@ -213,7 +196,10 @@ async def game_websocket(
213196 # Close the old WebSocket connection if it exists
214197 if old_ws :
215198 try :
216- await old_ws .close (code = 4000 , reason = "Reconnected from another session" )
199+ await old_ws .close (
200+ code = 4000 ,
201+ reason = "Reconnected from another session. Please close this tab." ,
202+ )
217203 except Exception :
218204 pass
219205
@@ -402,19 +388,3 @@ async def game_websocket(
402388 pass
403389 except Exception as _ :
404390 print (f"Error in game websocket: { traceback .format_exc ()} " )
405-
406-
407- @router .get ("/current-game" , response_model = Optional [GameView ])
408- async def get_current_game (
409- current_user : User = Depends (get_current_user ),
410- ) -> Optional [GameView ]:
411- """
412- Check if the user is in a game and return the game state. Used for reconnection.
413-
414- :param current_user: User object
415- :return: GameView object if the user is in a game, None otherwise
416- """
417- game_state = game_manager .get_player_game (current_user .id )
418- if game_state :
419- return game_manager .create_game_view (game_state , current_user .id )
420- return None
0 commit comments