Skip to content

Commit d9bb259

Browse files
authored
Merge pull request #64 from beatcode-official/refactor-code
fix: refactor code for better extensibility
2 parents 8a990f3 + 1a6fcc8 commit d9bb259

17 files changed

Lines changed: 657 additions & 539 deletions

File tree

app/api/endpoints/game/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from api.endpoints.game.controller import router as http_router
2+
from api.endpoints.game.websockets import router as ws_router
3+
4+
__all__ = ["http_router", "ws_router"]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from typing import Optional
2+
3+
from fastapi import APIRouter, Depends
4+
from schemas.game import GameView
5+
from db.models.user import User
6+
from api.endpoints.users.controller import get_current_user
7+
from services.game.manager import game_manager
8+
9+
router = APIRouter(prefix="/game", tags=["game"])
10+
11+
12+
@router.get("/current-game", response_model=Optional[GameView])
13+
async def get_current_game(
14+
current_user: User = Depends(get_current_user),
15+
) -> Optional[GameView]:
16+
"""
17+
Check if the user is in a game and return the game state. Used for reconnection.
18+
19+
:param current_user: User object
20+
:return: GameView object if the user is in a game, None otherwise
21+
"""
22+
game_state = game_manager.get_player_game(current_user.id)
23+
if game_state:
24+
return game_manager.create_game_view(game_state, current_user.id)
25+
return None
Lines changed: 90 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
import asyncio
22
import time
33
import 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
710
from core.config import settings
11+
from core.errors.game import SubmittingTooFastError
812
from db.models.user import User
913
from 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
1215
from services.execution.service import code_execution
1316
from services.game.ability import ability_manager
1417
from services.game.manager import game_manager
15-
from services.game.state import GameStatus
18+
from services.game.state import GameStatus, GameState
1619
from services.problem.service import ProblemManager
17-
from sqlalchemy.orm import Session
1820

1921
router = APIRouter(prefix="/game", tags=["game"])
2022
matchmaker = 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

Comments
 (0)