Chandima Prabhath commited on
Commit
3af4ccb
·
1 Parent(s): 48ca2cf
Files changed (2) hide show
  1. app.py +466 -0
  2. requirements.txt +2 -0
app.py ADDED
@@ -0,0 +1,466 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, BackgroundTasks
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from pydantic import BaseModel
4
+ from typing import Optional, List, Dict, Any, Tuple
5
+ import asyncio
6
+ import uuid
7
+ import json
8
+ from datetime import datetime
9
+ from enum import Enum
10
+
11
+ app = FastAPI(title="Multiplayer Checkers Backend")
12
+
13
+ # CORS middleware for frontend integration
14
+ app.add_middleware(
15
+ CORSMiddleware,
16
+ allow_origins=["*"],
17
+ allow_credentials=True,
18
+ allow_methods=["*"],
19
+ allow_headers=["*"],
20
+ )
21
+
22
+ # Game Models
23
+ class Player(int, Enum):
24
+ ONE = 1
25
+ TWO = 2
26
+
27
+ class PieceType(str, Enum):
28
+ REGULAR = "regular"
29
+ KING = "king"
30
+
31
+ class Piece(BaseModel):
32
+ player: Player
33
+ type: PieceType = PieceType.REGULAR
34
+
35
+ class Move(BaseModel):
36
+ from_pos: List[int] = [] # [row, col]
37
+ to: List[int] = [] # [row, col]
38
+ isJump: bool = False
39
+ jumpedPieceCoords: Optional[List[int]] = None
40
+
41
+ class GameMove(BaseModel):
42
+ player: Player
43
+ move: Move
44
+
45
+ class GameState(BaseModel):
46
+ board: List[List[Optional[Piece]]]
47
+ currentPlayer: Player
48
+ lastMove: Optional[Move] = None
49
+ winner: Optional[Player] = None
50
+ isMultiJumpPhase: bool = False
51
+ multiJumpPieceCoords: Optional[List[int]] = None
52
+ opponentDisconnected: bool = False
53
+
54
+ class MatchmakingRequest(BaseModel):
55
+ preferredPieceColor: Optional[str] = None
56
+
57
+ class MatchmakingCancel(BaseModel):
58
+ searchId: str
59
+
60
+ class LeaveGameRequest(BaseModel):
61
+ player: Player
62
+
63
+ # WebSocket Message Types
64
+ class WebSocketMessage(BaseModel):
65
+ type: str
66
+ payload: Optional[Dict[str, Any]] = None
67
+
68
+ # Global storage (in production, use Redis or database)
69
+ class GameManager:
70
+ def __init__(self):
71
+ self.games: Dict[str, GameState] = {}
72
+ self.game_connections: Dict[str, Dict[Player, WebSocket]] = {}
73
+ self.matchmaking_queue: List[str] = []
74
+ self.search_sessions: Dict[str, str] = {} # searchId -> playerId
75
+ self.player_searches: Dict[str, str] = {} # playerId -> searchId
76
+ self.pending_matches: Dict[str, Dict[str, Any]] = {} # searchId -> match details
77
+
78
+ def create_initial_board(self) -> List[List[Optional[Piece]]]:
79
+ """Create the initial 8x8 checkers board"""
80
+ board = [[None for _ in range(8)] for _ in range(8)]
81
+
82
+ # Place Player.ONE pieces (bottom)
83
+ for row in range(5, 8):
84
+ for col in range(8):
85
+ if (row + col) % 2 == 1: # Dark squares only
86
+ board[row][col] = Piece(player=Player.ONE)
87
+
88
+ # Place Player.TWO pieces (top)
89
+ for row in range(0, 3):
90
+ for col in range(8):
91
+ if (row + col) % 2 == 1: # Dark squares only
92
+ board[row][col] = Piece(player=Player.TWO)
93
+
94
+ return board
95
+
96
+ def create_game(self, room_id: str) -> GameState:
97
+ """Create a new game with initial state"""
98
+ game_state = GameState(
99
+ board=self.create_initial_board(),
100
+ currentPlayer=Player.ONE
101
+ )
102
+ self.games[room_id] = game_state
103
+ self.game_connections[room_id] = {}
104
+ return game_state
105
+
106
+ def is_valid_move(self, game_state: GameState, move: Move, player: Player) -> bool:
107
+ """Basic move validation (simplified for this implementation)"""
108
+ from_row, from_col = move.from_pos
109
+ to_row, to_col = move.to
110
+
111
+ # Check if it's the player's turn
112
+ if game_state.currentPlayer != player:
113
+ return False
114
+
115
+ # Check bounds
116
+ if not (0 <= from_row < 8 and 0 <= from_col < 8 and 0 <= to_row < 8 and 0 <= to_col < 8):
117
+ return False
118
+
119
+ # Check if there's a piece at the from position belonging to the player
120
+ piece = game_state.board[from_row][from_col]
121
+ if not piece or piece.player != player:
122
+ return False
123
+
124
+ # Check if destination is empty
125
+ if game_state.board[to_row][to_col] is not None:
126
+ return False
127
+
128
+ # Check if move is to a dark square (valid checkers squares)
129
+ if (to_row + to_col) % 2 == 0:
130
+ return False
131
+
132
+ # Basic move distance validation
133
+ row_diff = abs(to_row - from_row)
134
+ col_diff = abs(to_col - from_col)
135
+
136
+ if row_diff != col_diff: # Must be diagonal
137
+ return False
138
+
139
+ if row_diff == 1: # Regular move
140
+ return True
141
+ elif row_diff == 2: # Jump move
142
+ # Check if there's an opponent piece to jump over
143
+ mid_row = (from_row + to_row) // 2
144
+ mid_col = (from_col + to_col) // 2
145
+ mid_piece = game_state.board[mid_row][mid_col]
146
+
147
+ if mid_piece and mid_piece.player != player:
148
+ move.isJump = True
149
+ move.jumpedPieceCoords = [mid_row, mid_col]
150
+ return True
151
+
152
+ return False
153
+
154
+ def apply_move(self, game_state: GameState, move: Move, player: Player):
155
+ """Apply a move to the game state"""
156
+ from_row, from_col = move.from_pos
157
+ to_row, to_col = move.to
158
+
159
+ # Move the piece
160
+ piece = game_state.board[from_row][from_col]
161
+ game_state.board[from_row][from_col] = None
162
+ game_state.board[to_row][to_col] = piece
163
+
164
+ # Handle jump
165
+ if move.isJump and move.jumpedPieceCoords:
166
+ jump_row, jump_col = move.jumpedPieceCoords
167
+ game_state.board[jump_row][jump_col] = None
168
+
169
+ # Check for king promotion
170
+ if piece:
171
+ if (player == Player.ONE and to_row == 0) or (player == Player.TWO and to_row == 7):
172
+ piece.type = PieceType.KING
173
+
174
+ # Update game state
175
+ game_state.lastMove = move
176
+ game_state.currentPlayer = Player.TWO if player == Player.ONE else Player.ONE
177
+
178
+ # Check for winner (simplified - just check if opponent has no pieces)
179
+ self.check_winner(game_state)
180
+
181
+ def check_winner(self, game_state: GameState):
182
+ """Check if there's a winner"""
183
+ player_one_pieces = 0
184
+ player_two_pieces = 0
185
+
186
+ for row in game_state.board:
187
+ for piece in row:
188
+ if piece:
189
+ if piece.player == Player.ONE:
190
+ player_one_pieces += 1
191
+ else:
192
+ player_two_pieces += 1
193
+
194
+ if player_one_pieces == 0:
195
+ game_state.winner = Player.TWO
196
+ elif player_two_pieces == 0:
197
+ game_state.winner = Player.ONE
198
+
199
+ async def broadcast_game_update(self, room_id: str):
200
+ """Broadcast game state to all connected players"""
201
+ if room_id in self.game_connections:
202
+ game_state = self.games.get(room_id)
203
+ if game_state:
204
+ message = {
205
+ "type": "GAME_UPDATE",
206
+ "payload": game_state.dict()
207
+ }
208
+
209
+ disconnected = []
210
+ for player, websocket in self.game_connections[room_id].items():
211
+ try:
212
+ await websocket.send_text(json.dumps(message))
213
+ except:
214
+ disconnected.append(player)
215
+
216
+ # Clean up disconnected players
217
+ for player in disconnected:
218
+ del self.game_connections[room_id][player]
219
+
220
+ game_manager = GameManager()
221
+
222
+ # REST API Endpoints
223
+
224
+ @app.post("/api/matchmaking/find")
225
+ async def find_match(request: MatchmakingRequest = MatchmakingRequest()):
226
+ """Find a match for multiplayer game"""
227
+ search_id = str(uuid.uuid4())
228
+ player_id = str(uuid.uuid4())
229
+
230
+ game_manager.search_sessions[search_id] = player_id
231
+ game_manager.player_searches[player_id] = search_id
232
+
233
+ if len(game_manager.matchmaking_queue) > 0:
234
+ # Match found with waiting player (this is the second player)
235
+ player1_player_id = game_manager.matchmaking_queue.pop(0) # First player from queue
236
+ room_id = str(uuid.uuid4())
237
+
238
+ # Create new game
239
+ game_manager.create_game(room_id)
240
+
241
+ # Assign roles
242
+ player1_role = Player.ONE # First player gets Player.ONE
243
+ player2_role = Player.TWO # Second player (current caller) gets Player.TWO
244
+
245
+ # Get the first player's search ID
246
+ player1_search_id = game_manager.player_searches.get(player1_player_id)
247
+
248
+ # Store match details for first player to be retrieved via polling
249
+ if player1_search_id:
250
+ game_manager.pending_matches[player1_search_id] = {
251
+ "roomId": room_id,
252
+ "assignedPlayer": player1_role.value
253
+ }
254
+
255
+ # Clean up first player's search sessions
256
+ del game_manager.search_sessions[player1_search_id]
257
+ del game_manager.player_searches[player1_player_id]
258
+
259
+ # Clean up second player's search sessions
260
+ del game_manager.search_sessions[search_id]
261
+ del game_manager.player_searches[player_id]
262
+
263
+ # Return match details immediately to second player
264
+ return {
265
+ "status": "matched",
266
+ "roomId": room_id,
267
+ "assignedPlayer": player2_role.value
268
+ }
269
+ else:
270
+ # Add to queue (this is the first player)
271
+ game_manager.matchmaking_queue.append(player_id)
272
+ return {
273
+ "status": "searching",
274
+ "searchId": search_id
275
+ }
276
+
277
+ @app.get("/api/matchmaking/check/{search_id}")
278
+ async def check_match_status(search_id: str):
279
+ """Check if a match has been found for a specific search ID"""
280
+ # Check if match details are available for this search ID
281
+ if search_id in game_manager.pending_matches:
282
+ # Retrieve match details
283
+ match_details = game_manager.pending_matches[search_id]
284
+
285
+ # Remove the entry as it's a one-time fetch
286
+ del game_manager.pending_matches[search_id]
287
+
288
+ return {
289
+ "status": "matched",
290
+ "roomId": match_details["roomId"],
291
+ "assignedPlayer": match_details["assignedPlayer"]
292
+ }
293
+ else:
294
+ # Check if search is still valid/pending
295
+ if search_id in game_manager.search_sessions:
296
+ return {"status": "searching"}
297
+ else:
298
+ return {"status": "not_found"}
299
+
300
+ @app.post("/api/matchmaking/cancel")
301
+ async def cancel_matchmaking(request: MatchmakingCancel):
302
+ """Cancel matchmaking search"""
303
+ search_id = request.searchId
304
+
305
+ if search_id in game_manager.search_sessions:
306
+ player_id = game_manager.search_sessions[search_id]
307
+
308
+ # Remove from queue if present
309
+ if player_id in game_manager.matchmaking_queue:
310
+ game_manager.matchmaking_queue.remove(player_id)
311
+
312
+ # Clean up search sessions
313
+ del game_manager.search_sessions[search_id]
314
+ if player_id in game_manager.player_searches:
315
+ del game_manager.player_searches[player_id]
316
+
317
+ return {"status": "cancelled"}
318
+ else:
319
+ raise HTTPException(status_code=404, detail="Search not found")
320
+
321
+ @app.post("/api/game/{room_id}/move")
322
+ async def make_move(room_id: str, move_request: GameMove):
323
+ """Submit a player move"""
324
+ if room_id not in game_manager.games:
325
+ raise HTTPException(status_code=404, detail="Game room not found")
326
+
327
+ game_state = game_manager.games[room_id]
328
+
329
+ if not game_manager.is_valid_move(game_state, move_request.move, move_request.player):
330
+ raise HTTPException(status_code=400, detail="Invalid move")
331
+
332
+ # Apply the move
333
+ game_manager.apply_move(game_state, move_request.move, move_request.player)
334
+
335
+ # Broadcast update to connected players
336
+ await game_manager.broadcast_game_update(room_id)
337
+
338
+ return {"status": "success"}
339
+
340
+ @app.get("/api/game/{room_id}/state")
341
+ async def get_game_state(room_id: str, player: int):
342
+ """Get current game state (polling method - WebSockets recommended)"""
343
+ if room_id not in game_manager.games:
344
+ raise HTTPException(status_code=404, detail="Game not found")
345
+
346
+ game_state = game_manager.games[room_id]
347
+ return game_state.dict()
348
+
349
+ @app.post("/api/game/{room_id}/leave")
350
+ async def leave_game(room_id: str, request: LeaveGameRequest):
351
+ """Leave the game"""
352
+ if room_id not in game_manager.games:
353
+ raise HTTPException(status_code=404, detail="Game room not found")
354
+
355
+ game_state = game_manager.games[room_id]
356
+
357
+ # Set opponent as winner
358
+ game_state.winner = Player.TWO if request.player == Player.ONE else Player.ONE
359
+ game_state.opponentDisconnected = True
360
+
361
+ # Broadcast update
362
+ await game_manager.broadcast_game_update(room_id)
363
+
364
+ # Notify remaining player
365
+ if room_id in game_manager.game_connections:
366
+ for player, websocket in game_manager.game_connections[room_id].items():
367
+ if player != request.player:
368
+ try:
369
+ await websocket.send_text(json.dumps({
370
+ "type": "OPPONENT_LEFT"
371
+ }))
372
+ except:
373
+ pass
374
+
375
+ return {"status": "opponent_won_by_forfeit"}
376
+
377
+ # WebSocket Endpoints
378
+
379
+ @app.websocket("/ws/game/{room_id}/{player_id}")
380
+ async def websocket_game_endpoint(websocket: WebSocket, room_id: str, player_id: int):
381
+ """WebSocket connection for real-time game updates"""
382
+ await websocket.accept()
383
+
384
+ player = Player(player_id)
385
+
386
+ # Add to connections
387
+ if room_id not in game_manager.game_connections:
388
+ game_manager.game_connections[room_id] = {}
389
+
390
+ game_manager.game_connections[room_id][player] = websocket
391
+
392
+ # Send initial game state
393
+ if room_id in game_manager.games:
394
+ game_state = game_manager.games[room_id]
395
+ await websocket.send_text(json.dumps({
396
+ "type": "GAME_UPDATE",
397
+ "payload": game_state.dict()
398
+ }))
399
+
400
+ try:
401
+ while True:
402
+ # Receive message from client
403
+ data = await websocket.receive_text()
404
+ message = json.loads(data)
405
+
406
+ if message["type"] == "MAKE_MOVE":
407
+ try:
408
+ move_data = message["payload"]["move"]
409
+ move = Move(
410
+ from_pos=move_data["from_pos"],
411
+ to=move_data["to"],
412
+ isJump=move_data.get("isJump", False),
413
+ jumpedPieceCoords=move_data.get("jumpedPieceCoords")
414
+ )
415
+
416
+ if room_id in game_manager.games:
417
+ game_state = game_manager.games[room_id]
418
+
419
+ if game_manager.is_valid_move(game_state, move, player):
420
+ game_manager.apply_move(game_state, move, player)
421
+ await game_manager.broadcast_game_update(room_id)
422
+ else:
423
+ await websocket.send_text(json.dumps({
424
+ "type": "ERROR",
425
+ "payload": {"message": "Invalid move"}
426
+ }))
427
+
428
+ except Exception as e:
429
+ await websocket.send_text(json.dumps({
430
+ "type": "ERROR",
431
+ "payload": {"message": f"Move processing error: {str(e)}"}
432
+ }))
433
+
434
+ elif message["type"] == "LEAVE_MATCH":
435
+ if room_id in game_manager.games:
436
+ game_state = game_manager.games[room_id]
437
+ game_state.winner = Player.TWO if player == Player.ONE else Player.ONE
438
+ game_state.opponentDisconnected = True
439
+
440
+ await game_manager.broadcast_game_update(room_id)
441
+ break
442
+
443
+ except WebSocketDisconnect:
444
+ # Handle disconnect
445
+ if room_id in game_manager.game_connections and player in game_manager.game_connections[room_id]:
446
+ del game_manager.game_connections[room_id][player]
447
+
448
+ # Notify opponent of disconnect
449
+ if room_id in game_manager.games:
450
+ game_state = game_manager.games[room_id]
451
+ game_state.opponentDisconnected = True
452
+ await game_manager.broadcast_game_update(room_id)
453
+
454
+ # Health check endpoint
455
+ @app.get("/health")
456
+ async def health_check():
457
+ return {"status": "healthy", "timestamp": datetime.now().isoformat()}
458
+
459
+ # Root endpoint
460
+ @app.get("/")
461
+ async def root():
462
+ return {"message": "Multiplayer Checkers Backend API", "version": "1.0.0"}
463
+
464
+ if __name__ == "__main__":
465
+ import uvicorn
466
+ uvicorn.run(app, host="0.0.0.0", port=8000)
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ fastapi
2
+ uvicorn