Phase 5: Complete Core Game Engine implementation
- Added MTGOnlineGame, GamePlayer, GameHistory models - Implemented full game CRUD endpoints (create, list, get, join, leave, start, end) - Integrated WebSocket game server with database persistence - Added Commander deck validation service (100-card min, color identity, ban list) - Created Alembic migration 006 for new game tables - All game endpoints now database-backed (no more stubs)
This commit is contained in:
@@ -1,9 +1,19 @@
|
||||
"""WebSocket game server for real-time multiplayer gameplay."""
|
||||
"""WebSocket game server for real-time multiplayer gameplay.
|
||||
|
||||
Integrates with SQLAlchemy database for game state persistence.
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, Set, Optional
|
||||
from datetime import datetime
|
||||
from fastapi import WebSocket, WebSocketDisconnect
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.models.models import MTGOnlineGame, GamePlayer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Protocol message types
|
||||
GAME_EVENT_JOIN = 1000
|
||||
@@ -75,7 +85,10 @@ GAME_COMMAND_REVERSE_TURN = 1034
|
||||
|
||||
|
||||
class GameRoom:
|
||||
"""Manages a single game room with multiple connected players."""
|
||||
"""Manages a single game room with multiple connected players.
|
||||
|
||||
Integrates with database for state persistence.
|
||||
"""
|
||||
|
||||
def __init__(self, room_id: int, game_id: int):
|
||||
self.room_id = room_id
|
||||
@@ -92,6 +105,38 @@ class GameRoom:
|
||||
"zones": {},
|
||||
}
|
||||
self.lock = asyncio.Lock()
|
||||
self.db_session: Optional[AsyncSession] = None
|
||||
|
||||
async def initialize_db(self, db_session: AsyncSession):
|
||||
"""Initialize database session and load game state."""
|
||||
self.db_session = db_session
|
||||
await self._load_game_state()
|
||||
|
||||
async def _load_game_state(self):
|
||||
"""Load game state from database."""
|
||||
if not self.db_session:
|
||||
return
|
||||
|
||||
try:
|
||||
# Load game
|
||||
stmt = select(MTGOnlineGame).where(MTGOnlineGame.id == self.game_id)
|
||||
result = await self.db_session.execute(stmt)
|
||||
game = result.scalar_one_or_none()
|
||||
|
||||
if game:
|
||||
self.state["started"] = game.state == "IN_PROGRESS"
|
||||
self.state["password_protected"] = game.is_password_protected
|
||||
|
||||
# Load players
|
||||
player_stmt = select(GamePlayer).where(GamePlayer.game_id == self.game_id)
|
||||
player_result = await self.db_session.execute(player_stmt)
|
||||
players = player_result.scalars().all()
|
||||
|
||||
for player in players:
|
||||
if player.is_host:
|
||||
self.host_id = player.user_id
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading game state: {e}")
|
||||
|
||||
async def add_player(self, player_id: int, websocket: WebSocket):
|
||||
"""Add a player to the game."""
|
||||
@@ -114,7 +159,7 @@ class GameRoom:
|
||||
try:
|
||||
await websocket.send_text(message_str)
|
||||
except Exception as e:
|
||||
print(f"Error sending to player {player_id}: {e}")
|
||||
logger.error(f"Error sending to player {player_id}: {e}")
|
||||
|
||||
async def send_to_player(self, player_id: int, message: dict):
|
||||
"""Send a message to a specific player."""
|
||||
@@ -123,7 +168,7 @@ class GameRoom:
|
||||
message_str = json.dumps(message)
|
||||
await self.players[player_id].send_text(message_str)
|
||||
except Exception as e:
|
||||
print(f"Error sending to player {player_id}: {e}")
|
||||
logger.error(f"Error sending to player {player_id}: {e}")
|
||||
|
||||
def get_player_count(self) -> int:
|
||||
"""Get number of connected players."""
|
||||
@@ -132,6 +177,54 @@ class GameRoom:
|
||||
def is_full(self, max_players: int = 4) -> bool:
|
||||
"""Check if game is full."""
|
||||
return self.get_player_count() >= max_players
|
||||
|
||||
async def update_game_state(self, state: str):
|
||||
"""Update game state in database."""
|
||||
if not self.db_session:
|
||||
return
|
||||
|
||||
try:
|
||||
stmt = select(MTGOnlineGame).where(MTGOnlineGame.id == self.game_id)
|
||||
result = await self.db_session.execute(stmt)
|
||||
game = result.scalar_one_or_none()
|
||||
|
||||
if game:
|
||||
game.state = state
|
||||
if state == "IN_PROGRESS" and not game.started_at:
|
||||
game.started_at = datetime.now()
|
||||
elif state == "FINISHED":
|
||||
game.finished_at = datetime.now()
|
||||
|
||||
await self.db_session.flush()
|
||||
logger.info(f"Game {self.game_id} state updated to {state}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating game state: {e}")
|
||||
|
||||
async def update_player_ready(self, player_id: int, is_ready: bool):
|
||||
"""Update player ready status in database."""
|
||||
if not self.db_session:
|
||||
return
|
||||
|
||||
try:
|
||||
stmt = select(GamePlayer).where(
|
||||
GamePlayer.game_id == self.game_id,
|
||||
GamePlayer.user_id == player_id,
|
||||
)
|
||||
result = await self.db_session.execute(stmt)
|
||||
player = result.scalar_one_or_none()
|
||||
|
||||
if player:
|
||||
player.is_ready = is_ready
|
||||
await self.db_session.flush()
|
||||
|
||||
if is_ready:
|
||||
self.state["players_ready"].add(player_id)
|
||||
else:
|
||||
self.state["players_ready"].discard(player_id)
|
||||
|
||||
logger.info(f"Player {player_id} ready status updated to {is_ready}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating player ready status: {e}")
|
||||
|
||||
|
||||
class GameServer:
|
||||
@@ -175,7 +268,10 @@ game_server = GameServer()
|
||||
|
||||
|
||||
async def game_websocket_endpoint(websocket: WebSocket, game_id: int):
|
||||
"""WebSocket endpoint for game connections."""
|
||||
"""WebSocket endpoint for game connections.
|
||||
|
||||
Integrates with database for state persistence.
|
||||
"""
|
||||
room = await game_server.get_room(game_id)
|
||||
if not room:
|
||||
await websocket.close(code=4004, reason="Game not found")
|
||||
@@ -249,7 +345,7 @@ async def game_websocket_endpoint(websocket: WebSocket, game_id: int):
|
||||
"player_id": player_id,
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Error in game WebSocket: {e}")
|
||||
logger.error(f"Error in game WebSocket: {e}")
|
||||
await room.remove_player(player_id)
|
||||
|
||||
|
||||
@@ -300,6 +396,11 @@ async def process_game_command(room: GameRoom, player_id: int, command: dict):
|
||||
})
|
||||
return
|
||||
|
||||
# Handle ready command specially
|
||||
if cmd_type == GAME_COMMAND_READY_START:
|
||||
is_ready = command.get("data", {}).get("is_ready", False)
|
||||
await room.update_player_ready(player_id, is_ready)
|
||||
|
||||
# Broadcast command as game event
|
||||
await room.broadcast({
|
||||
"type": "game_event",
|
||||
|
||||
Reference in New Issue
Block a user