WebSocket Message Schemas (WS-002): - Add Pydantic models for all client/server WebSocket messages - Implement discriminated unions for message type parsing - Include JoinGame, Action, Resign, Heartbeat client messages - Include GameState, ActionResult, Error, TurnStart server messages Connection Manager (WS-003): - Add Redis-backed WebSocket connection tracking - Implement user-to-sid mapping with TTL management - Support game room association and opponent lookup - Add heartbeat tracking for connection health Socket.IO Authentication (WS-004): - Add JWT-based authentication middleware - Support token extraction from multiple formats - Implement session setup with ConnectionManager integration - Add require_auth helper for event handlers Socket.IO Server Setup (WS-001): - Configure AsyncServer with ASGI mode - Register /game namespace with event handlers - Integrate with FastAPI via ASGIApp wrapper - Configure CORS from application settings Game Service (GS-001): - Add stateless GameService for game lifecycle orchestration - Create engine per-operation using rules from GameState - Implement action-based RNG seeding for deterministic replay - Add rng_seed field to GameState for replay support Architecture verified: - Core module independence (no forbidden imports) - Config from request pattern (rules in GameState) - Dependency injection (constructor deps, method config) - All 1090 tests passing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
694 lines
27 KiB
JSON
694 lines
27 KiB
JSON
{
|
|
"meta": {
|
|
"version": "1.0.0",
|
|
"created": "2026-01-28",
|
|
"lastUpdated": "2026-01-28",
|
|
"planType": "phase",
|
|
"phaseId": "PHASE_4",
|
|
"phaseName": "Game Service + WebSocket",
|
|
"description": "Real-time gameplay infrastructure - WebSocket communication, game lifecycle management, reconnection handling, and turn timeout system",
|
|
"totalEstimatedHours": 45,
|
|
"totalTasks": 18,
|
|
"completedTasks": 5,
|
|
"status": "in_progress",
|
|
"masterPlan": "../PROJECT_PLAN_MASTER.json"
|
|
},
|
|
|
|
"goals": [
|
|
"Implement WebSocket server using python-socketio for real-time bidirectional communication",
|
|
"Create GameService to orchestrate game lifecycle (create, join, execute actions, resume, end)",
|
|
"Design message protocol specification for all client-server communication",
|
|
"Implement reconnection handling with session recovery",
|
|
"Build turn timeout system with configurable time limits",
|
|
"Integrate with existing GameEngine, GameStateManager, and DeckService",
|
|
"Capture replay data from game start for future replay feature",
|
|
"Implement spectator mode (stretch goal)"
|
|
],
|
|
|
|
"architectureNotes": {
|
|
"webSocketLibrary": {
|
|
"choice": "python-socketio",
|
|
"rationale": "Mature Socket.IO implementation, handles rooms/namespaces, auto-reconnection on client, ASGI integration with FastAPI",
|
|
"alternatives": ["starlette.websockets (raw WS)", "fastapi-websocket-rpc"],
|
|
"documentation": "https://python-socketio.readthedocs.io/"
|
|
},
|
|
"socketIOIntegration": {
|
|
"pattern": "Mount Socket.IO as ASGI app alongside FastAPI",
|
|
"namespaces": {
|
|
"/game": "Active game communication (actions, state updates)",
|
|
"/lobby": "Pre-game lobby (matchmaking, invites) - Phase 6"
|
|
},
|
|
"rooms": {
|
|
"game:{game_id}": "All participants of a specific game",
|
|
"spectators:{game_id}": "Spectators watching a game"
|
|
}
|
|
},
|
|
"messageProtocol": {
|
|
"format": "JSON with type discriminator",
|
|
"clientToServer": [
|
|
"game:action - Execute a game action (attack, play_pokemon, etc.)",
|
|
"game:join - Join/rejoin a game session",
|
|
"game:leave - Leave current game",
|
|
"game:resign - Resign from game",
|
|
"game:heartbeat - Keep connection alive"
|
|
],
|
|
"serverToClient": [
|
|
"game:state - Full/delta game state update",
|
|
"game:action_result - Result of player action",
|
|
"game:error - Error message",
|
|
"game:turn_start - New turn notification",
|
|
"game:turn_timeout - Turn timeout warning/expiration",
|
|
"game:game_over - Game ended notification",
|
|
"game:opponent_connected - Opponent connection status",
|
|
"game:opponent_action - Opponent performed action (for animations)"
|
|
]
|
|
},
|
|
"connectionState": {
|
|
"storage": "Redis hash per connection",
|
|
"keyPattern": "conn:{sid} -> {user_id, game_id, connected_at}",
|
|
"sessionRecovery": "On reconnect, client sends last_event_id to resume from"
|
|
},
|
|
"turnTimeout": {
|
|
"implementation": "Redis EXPIRE + background task polling",
|
|
"defaultTimeout": 180,
|
|
"warningAt": 30,
|
|
"graceOnReconnect": 15,
|
|
"actions": ["timeout_warning", "auto_pass", "loss_by_timeout"]
|
|
},
|
|
"persistenceStrategy": {
|
|
"everyAction": "Redis only (fast path) via GameStateManager.save_to_cache()",
|
|
"turnBoundary": "Redis + Postgres via GameStateManager.persist_to_db()",
|
|
"gameEnd": "Move to GameHistory, delete from ActiveGame"
|
|
},
|
|
"existingComponents": {
|
|
"GameEngine": "app/core/engine.py - validate/execute actions, create games",
|
|
"GameStateManager": "app/services/game_state_manager.py - Redis/Postgres persistence",
|
|
"ActionModels": "app/core/models/actions.py - all action types",
|
|
"Visibility": "app/core/visibility.py - filter state per player",
|
|
"ActiveGame": "app/db/models/game.py - Postgres backup model",
|
|
"GameHistory": "app/db/models/game.py - completed game records",
|
|
"DeckService": "app/services/deck_service.py - load player decks"
|
|
}
|
|
},
|
|
|
|
"directoryStructure": {
|
|
"socketio": "backend/app/socketio/",
|
|
"services": "backend/app/services/",
|
|
"schemas": "backend/app/schemas/",
|
|
"tests": "backend/tests/socketio/, backend/tests/services/"
|
|
},
|
|
|
|
"tasks": [
|
|
{
|
|
"id": "WS-001",
|
|
"name": "Set up python-socketio with FastAPI",
|
|
"description": "Install and configure python-socketio ASGI server, mount alongside FastAPI app",
|
|
"category": "infrastructure",
|
|
"priority": 1,
|
|
"completed": true,
|
|
"tested": true,
|
|
"dependencies": [],
|
|
"files": [
|
|
{"path": "app/socketio/__init__.py", "status": "create"},
|
|
{"path": "app/socketio/server.py", "status": "create"},
|
|
{"path": "app/main.py", "status": "modify"}
|
|
],
|
|
"details": [
|
|
"Install python-socketio[asgi] via uv",
|
|
"Create AsyncServer with async_mode='asgi'",
|
|
"Create ASGI app combining FastAPI and Socket.IO",
|
|
"Configure CORS for Socket.IO to match FastAPI settings",
|
|
"Add /game namespace skeleton",
|
|
"Update app/main.py to use combined ASGI app"
|
|
],
|
|
"estimatedHours": 2,
|
|
"notes": "Consider using engineio.async_drivers for uvicorn compatibility"
|
|
},
|
|
{
|
|
"id": "WS-002",
|
|
"name": "Create WebSocket message schemas",
|
|
"description": "Define Pydantic models for all WebSocket message types",
|
|
"category": "schemas",
|
|
"priority": 2,
|
|
"completed": true,
|
|
"tested": true,
|
|
"dependencies": ["WS-001"],
|
|
"files": [
|
|
{"path": "app/schemas/ws_messages.py", "status": "create"}
|
|
],
|
|
"details": [
|
|
"Base message model with 'type' discriminator",
|
|
"Client messages: JoinGameMessage, ActionMessage, ResignMessage, HeartbeatMessage",
|
|
"Server messages: GameStateMessage, ActionResultMessage, ErrorMessage, TurnStartMessage",
|
|
"Server messages: TurnTimeoutMessage, GameOverMessage, OpponentStatusMessage",
|
|
"Include message_id for idempotency and event ordering",
|
|
"Include timestamp on all server messages"
|
|
],
|
|
"estimatedHours": 2,
|
|
"notes": "Use Pydantic discriminated unions for message parsing"
|
|
},
|
|
{
|
|
"id": "WS-003",
|
|
"name": "Create ConnectionManager service",
|
|
"description": "Manage WebSocket connections with Redis-backed session tracking",
|
|
"category": "services",
|
|
"priority": 3,
|
|
"completed": true,
|
|
"tested": true,
|
|
"dependencies": ["WS-001"],
|
|
"files": [
|
|
{"path": "app/services/connection_manager.py", "status": "create"}
|
|
],
|
|
"details": [
|
|
"Track connection state in Redis: conn:{sid} -> {user_id, game_id, connected_at}",
|
|
"Map user_id to active sid: user_conn:{user_id} -> sid",
|
|
"Track game participants: game_conns:{game_id} -> set of sids",
|
|
"Handle connect/disconnect lifecycle",
|
|
"Detect stale connections (heartbeat timeout)",
|
|
"Provide methods: get_user_connection, get_game_connections, is_user_online",
|
|
"Clean up on disconnect"
|
|
],
|
|
"estimatedHours": 3,
|
|
"notes": "Use Redis HSET/HGET for connection data, SADD/SREM for sets"
|
|
},
|
|
{
|
|
"id": "WS-004",
|
|
"name": "Implement Socket.IO authentication middleware",
|
|
"description": "Authenticate WebSocket connections using JWT tokens",
|
|
"category": "auth",
|
|
"priority": 4,
|
|
"completed": true,
|
|
"tested": true,
|
|
"dependencies": ["WS-001", "WS-003"],
|
|
"files": [
|
|
{"path": "app/socketio/auth.py", "status": "create"},
|
|
{"path": "app/socketio/server.py", "status": "modify"}
|
|
],
|
|
"details": [
|
|
"Extract JWT from connection handshake (auth header or query param)",
|
|
"Validate token using existing JWTService",
|
|
"Load user from database and attach to socket session",
|
|
"Reject connection if token invalid/expired",
|
|
"Handle token refresh during long sessions",
|
|
"Store user_id in socket session for subsequent events"
|
|
],
|
|
"estimatedHours": 2,
|
|
"notes": "Socket.IO provides auth callback in connect event"
|
|
},
|
|
{
|
|
"id": "GS-001",
|
|
"name": "Create GameService skeleton",
|
|
"description": "Service layer orchestrating game lifecycle between WebSocket and GameEngine",
|
|
"category": "services",
|
|
"priority": 5,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["WS-002"],
|
|
"files": [
|
|
{"path": "app/services/game_service.py", "status": "create"}
|
|
],
|
|
"details": [
|
|
"Constructor injection: GameEngine, GameStateManager, DeckService, CardService",
|
|
"Core methods: create_game, join_game, execute_action, resign_game, end_game",
|
|
"Helper methods: get_game_state, get_player_view, is_player_turn",
|
|
"Error handling with custom exceptions: GameNotFoundError, NotPlayerTurnError, InvalidActionError",
|
|
"All methods async for database/cache operations",
|
|
"No direct Socket.IO dependency - returns results for WS layer to emit"
|
|
],
|
|
"estimatedHours": 2,
|
|
"notes": "Service is stateless - all state in Redis/Postgres via GameStateManager"
|
|
},
|
|
{
|
|
"id": "GS-002",
|
|
"name": "Implement GameService.create_game",
|
|
"description": "Create new game from player decks, initialize in Redis and Postgres",
|
|
"category": "services",
|
|
"priority": 6,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["GS-001"],
|
|
"files": [
|
|
{"path": "app/services/game_service.py", "status": "modify"}
|
|
],
|
|
"details": [
|
|
"Accept player IDs, deck IDs, game type, optional rules config",
|
|
"Load decks via DeckService, convert to CardInstances",
|
|
"Load card registry from CardService",
|
|
"Call GameEngine.create_game() to initialize game state",
|
|
"Save to Redis via GameStateManager.save_to_cache()",
|
|
"Persist to Postgres via GameStateManager.persist_to_db()",
|
|
"Return game_id and initial player-visible states",
|
|
"Handle campaign mode (vs NPC) - defer to Phase 5"
|
|
],
|
|
"estimatedHours": 3,
|
|
"notes": "CardInstances need unique IDs - use UUID per card"
|
|
},
|
|
{
|
|
"id": "GS-003",
|
|
"name": "Implement GameService.execute_action",
|
|
"description": "Validate and execute player actions, persist state, return results",
|
|
"category": "services",
|
|
"priority": 7,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["GS-002"],
|
|
"files": [
|
|
{"path": "app/services/game_service.py", "status": "modify"}
|
|
],
|
|
"details": [
|
|
"Load game state from cache (GameStateManager.load_state())",
|
|
"Verify it's the player's turn",
|
|
"Parse action from message into Action model",
|
|
"Call GameEngine.execute_action()",
|
|
"If action ends turn, handle turn boundary (persist to DB, process status effects)",
|
|
"Save updated state to cache",
|
|
"If game over, call end_game()",
|
|
"Return ActionResult with state changes for animation"
|
|
],
|
|
"estimatedHours": 4,
|
|
"notes": "Handle forced actions (select new active after KO, prize selection)"
|
|
},
|
|
{
|
|
"id": "GS-004",
|
|
"name": "Implement GameService.join_game and resume_game",
|
|
"description": "Handle players joining/rejoining games",
|
|
"category": "services",
|
|
"priority": 8,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["GS-002"],
|
|
"files": [
|
|
{"path": "app/services/game_service.py", "status": "modify"}
|
|
],
|
|
"details": [
|
|
"join_game: For new games awaiting opponent (Phase 6 matchmaking)",
|
|
"resume_game: Reconnect to existing game after disconnect",
|
|
"Verify player is participant in the game",
|
|
"Return current game state filtered for player visibility",
|
|
"Include pending forced actions if any",
|
|
"Track last_event_id for event replay on reconnect"
|
|
],
|
|
"estimatedHours": 2,
|
|
"notes": "Resume needs to handle mid-turn reconnection"
|
|
},
|
|
{
|
|
"id": "GS-005",
|
|
"name": "Implement GameService.end_game",
|
|
"description": "Handle game completion - record history, cleanup active game",
|
|
"category": "services",
|
|
"priority": 9,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["GS-003"],
|
|
"files": [
|
|
{"path": "app/services/game_service.py", "status": "modify"}
|
|
],
|
|
"details": [
|
|
"Create GameHistory record with all game data",
|
|
"Include replay_data from game.action_log",
|
|
"Calculate duration_seconds from started_at",
|
|
"Delete from ActiveGame table",
|
|
"Delete from Redis cache",
|
|
"Return final result for clients",
|
|
"Handle campaign rewards - defer to Phase 5"
|
|
],
|
|
"estimatedHours": 2,
|
|
"notes": "GameHistory model already exists in app/db/models/game.py"
|
|
},
|
|
{
|
|
"id": "WS-005",
|
|
"name": "Implement /game namespace event handlers",
|
|
"description": "Socket.IO event handlers for game communication",
|
|
"category": "websocket",
|
|
"priority": 10,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["WS-004", "GS-003", "GS-004"],
|
|
"files": [
|
|
{"path": "app/socketio/game_namespace.py", "status": "create"},
|
|
{"path": "app/socketio/server.py", "status": "modify"}
|
|
],
|
|
"details": [
|
|
"Event: connect - Auth, register connection, check for resumable games",
|
|
"Event: disconnect - Update connection state, notify opponent",
|
|
"Event: game:join - Call GameService.resume_game(), emit state to player",
|
|
"Event: game:action - Parse action, call GameService.execute_action(), emit results",
|
|
"Event: game:resign - Call GameService.resign_game(), emit game_over",
|
|
"Event: game:heartbeat - Update last_seen timestamp",
|
|
"Emit to room on state changes so all participants receive updates",
|
|
"Send visibility-filtered state to each player"
|
|
],
|
|
"estimatedHours": 4,
|
|
"notes": "Each player sees their own visible state - use get_visible_state()"
|
|
},
|
|
{
|
|
"id": "WS-006",
|
|
"name": "Implement state broadcast helpers",
|
|
"description": "Utilities to broadcast game state to all participants with proper filtering",
|
|
"category": "websocket",
|
|
"priority": 11,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["WS-005"],
|
|
"files": [
|
|
{"path": "app/socketio/broadcast.py", "status": "create"}
|
|
],
|
|
"details": [
|
|
"broadcast_game_state: Send filtered state to each player in game room",
|
|
"broadcast_action_result: Send action outcome with animation data",
|
|
"broadcast_turn_change: Notify all players of turn change",
|
|
"broadcast_game_over: Send final results to all participants",
|
|
"notify_opponent_status: Tell player if opponent connected/disconnected",
|
|
"Use Socket.IO rooms for efficient broadcasting",
|
|
"Handle spectators (spectator view uses get_spectator_state)"
|
|
],
|
|
"estimatedHours": 2,
|
|
"notes": "Consider delta updates for bandwidth optimization (future)"
|
|
},
|
|
{
|
|
"id": "TO-001",
|
|
"name": "Create TurnTimeoutService",
|
|
"description": "Manage turn time limits with warnings and automatic actions",
|
|
"category": "services",
|
|
"priority": 12,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["GS-003", "WS-006"],
|
|
"files": [
|
|
{"path": "app/services/turn_timeout_service.py", "status": "create"}
|
|
],
|
|
"details": [
|
|
"Store turn deadline in Redis: turn_timeout:{game_id} -> deadline_timestamp",
|
|
"Methods: start_turn_timer, cancel_timer, get_remaining_time, extend_timer",
|
|
"Background task checks for expired timers (polling or Redis keyspace notifications)",
|
|
"When timer expires: emit warning at 30s, auto-pass or loss at 0s",
|
|
"Configurable timeout per game type (campaign more lenient)",
|
|
"Grace period on reconnect (15s extension)"
|
|
],
|
|
"estimatedHours": 4,
|
|
"notes": "Consider Redis EXPIRE with keyspace notification for efficiency"
|
|
},
|
|
{
|
|
"id": "TO-002",
|
|
"name": "Integrate timeout with GameService",
|
|
"description": "Start/stop turn timers on turn boundaries",
|
|
"category": "integration",
|
|
"priority": 13,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["TO-001", "GS-003"],
|
|
"files": [
|
|
{"path": "app/services/game_service.py", "status": "modify"},
|
|
{"path": "app/socketio/game_namespace.py", "status": "modify"}
|
|
],
|
|
"details": [
|
|
"Start timer after turn_start in execute_action",
|
|
"Cancel timer when action ends turn",
|
|
"Extend timer on reconnect",
|
|
"Handle timeout: auto-pass or declare loss based on config",
|
|
"Emit turn_timeout warning to current player",
|
|
"Update ActiveGame.turn_deadline for persistence"
|
|
],
|
|
"estimatedHours": 2,
|
|
"notes": "Timeout should trigger auto-pass first, loss only after N timeouts"
|
|
},
|
|
{
|
|
"id": "RC-001",
|
|
"name": "Implement reconnection flow",
|
|
"description": "Handle client reconnection to ongoing games",
|
|
"category": "reconnection",
|
|
"priority": 14,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["WS-005", "GS-004"],
|
|
"files": [
|
|
{"path": "app/socketio/game_namespace.py", "status": "modify"},
|
|
{"path": "app/services/connection_manager.py", "status": "modify"}
|
|
],
|
|
"details": [
|
|
"On connect: Check for active game via ConnectionManager/GameStateManager",
|
|
"Auto-rejoin game room if active game exists",
|
|
"Send full game state to reconnecting player",
|
|
"Include pending actions (forced_actions queue)",
|
|
"Extend turn timer by grace period",
|
|
"Notify opponent of reconnection",
|
|
"Handle rapid disconnect/reconnect (debounce notifications)"
|
|
],
|
|
"estimatedHours": 3,
|
|
"notes": "Client should store game_id locally for quick resume"
|
|
},
|
|
{
|
|
"id": "API-001",
|
|
"name": "Create REST endpoints for game management",
|
|
"description": "HTTP endpoints for game creation and status checks",
|
|
"category": "api",
|
|
"priority": 15,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["GS-002", "GS-004"],
|
|
"files": [
|
|
{"path": "app/api/games.py", "status": "create"},
|
|
{"path": "app/main.py", "status": "modify"}
|
|
],
|
|
"details": [
|
|
"POST /games - Create new game (returns game_id, connect via WebSocket)",
|
|
"GET /games/{game_id} - Get game info (for reconnection checks)",
|
|
"GET /games/me/active - List user's active games",
|
|
"POST /games/{game_id}/resign - Resign via HTTP (backup to WS)",
|
|
"Authentication required for all endpoints",
|
|
"Rate limiting on game creation"
|
|
],
|
|
"estimatedHours": 3,
|
|
"notes": "WebSocket is primary for gameplay, REST for management"
|
|
},
|
|
{
|
|
"id": "TEST-001",
|
|
"name": "Unit tests for GameService",
|
|
"description": "Test game lifecycle methods with mocked dependencies",
|
|
"category": "testing",
|
|
"priority": 16,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["GS-005"],
|
|
"files": [
|
|
{"path": "tests/unit/services/test_game_service.py", "status": "create"}
|
|
],
|
|
"details": [
|
|
"Test create_game with valid/invalid inputs",
|
|
"Test execute_action for all action types",
|
|
"Test turn boundary transitions",
|
|
"Test win condition detection",
|
|
"Test resume_game state recovery",
|
|
"Test end_game cleanup",
|
|
"Mock GameEngine, GameStateManager, DeckService"
|
|
],
|
|
"estimatedHours": 4,
|
|
"notes": "Use pytest-asyncio for async tests"
|
|
},
|
|
{
|
|
"id": "TEST-002",
|
|
"name": "Integration tests for WebSocket flow",
|
|
"description": "End-to-end tests for WebSocket game flow",
|
|
"category": "testing",
|
|
"priority": 17,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["WS-006", "RC-001"],
|
|
"files": [
|
|
{"path": "tests/socketio/test_game_namespace.py", "status": "create"},
|
|
{"path": "tests/socketio/conftest.py", "status": "create"}
|
|
],
|
|
"details": [
|
|
"Test connection with valid/invalid JWT",
|
|
"Test game:join for valid/invalid games",
|
|
"Test action execution and state broadcast",
|
|
"Test turn timeout flow",
|
|
"Test reconnection and state recovery",
|
|
"Test opponent disconnect notification",
|
|
"Use python-socketio test client",
|
|
"Require testcontainers for Redis/Postgres"
|
|
],
|
|
"estimatedHours": 5,
|
|
"notes": "Socket.IO test client simulates real connections"
|
|
},
|
|
{
|
|
"id": "OPT-001",
|
|
"name": "Implement spectator mode",
|
|
"description": "Allow users to watch ongoing games (stretch goal)",
|
|
"category": "optional",
|
|
"priority": 18,
|
|
"completed": false,
|
|
"tested": false,
|
|
"dependencies": ["WS-006"],
|
|
"files": [
|
|
{"path": "app/socketio/game_namespace.py", "status": "modify"},
|
|
{"path": "app/services/game_service.py", "status": "modify"}
|
|
],
|
|
"details": [
|
|
"Event: game:spectate - Join as spectator",
|
|
"Add spectators to spectators:{game_id} room",
|
|
"Spectators receive get_spectator_state() view (no hands visible)",
|
|
"Spectator count visible to players",
|
|
"No action permissions for spectators",
|
|
"Optional: Public/private game setting"
|
|
],
|
|
"estimatedHours": 3,
|
|
"notes": "Stretch goal - implement if time permits"
|
|
}
|
|
],
|
|
|
|
"acceptanceCriteria": [
|
|
{
|
|
"id": "AC-001",
|
|
"description": "WebSocket server accepts connections with valid JWT authentication",
|
|
"tasks": ["WS-001", "WS-004"],
|
|
"met": false
|
|
},
|
|
{
|
|
"id": "AC-002",
|
|
"description": "Game can be created via REST API and played via WebSocket",
|
|
"tasks": ["API-001", "GS-002", "WS-005"],
|
|
"met": false
|
|
},
|
|
{
|
|
"id": "AC-003",
|
|
"description": "All game actions are validated and executed correctly through WebSocket",
|
|
"tasks": ["GS-003", "WS-005"],
|
|
"met": false
|
|
},
|
|
{
|
|
"id": "AC-004",
|
|
"description": "Each player receives their own visibility-filtered game state",
|
|
"tasks": ["WS-006"],
|
|
"met": false
|
|
},
|
|
{
|
|
"id": "AC-005",
|
|
"description": "Turn timeouts emit warnings and trigger auto-pass/loss appropriately",
|
|
"tasks": ["TO-001", "TO-002"],
|
|
"met": false
|
|
},
|
|
{
|
|
"id": "AC-006",
|
|
"description": "Disconnected players can reconnect and resume their game",
|
|
"tasks": ["RC-001", "GS-004"],
|
|
"met": false
|
|
},
|
|
{
|
|
"id": "AC-007",
|
|
"description": "Game state persists to Postgres at turn boundaries for durability",
|
|
"tasks": ["GS-003", "GS-005"],
|
|
"met": false
|
|
},
|
|
{
|
|
"id": "AC-008",
|
|
"description": "Completed games are recorded in GameHistory with replay data",
|
|
"tasks": ["GS-005"],
|
|
"met": false
|
|
},
|
|
{
|
|
"id": "AC-009",
|
|
"description": "Unit and integration tests cover critical game flow paths",
|
|
"tasks": ["TEST-001", "TEST-002"],
|
|
"met": false
|
|
}
|
|
],
|
|
|
|
"risksMitigations": [
|
|
{
|
|
"risk": "Socket.IO scalability with many concurrent games",
|
|
"mitigation": "Redis adapter for Socket.IO enables horizontal scaling across multiple server instances",
|
|
"priority": "medium"
|
|
},
|
|
{
|
|
"risk": "Race conditions in action execution",
|
|
"mitigation": "Use Redis locks (SETNX) for game state during action execution",
|
|
"priority": "high"
|
|
},
|
|
{
|
|
"risk": "Stale connections consuming resources",
|
|
"mitigation": "Heartbeat mechanism with 30s interval, cleanup on missed heartbeats",
|
|
"priority": "medium"
|
|
},
|
|
{
|
|
"risk": "Turn timeout drift due to server restart",
|
|
"mitigation": "Store absolute deadline in Redis/Postgres, recalculate on startup",
|
|
"priority": "medium"
|
|
},
|
|
{
|
|
"risk": "ConnectionManager race condition on rapid reconnects",
|
|
"mitigation": "Consider Redis Lua script for atomic old-connection cleanup in register_connection. Low probability in practice but could cause connection tracking issues during rapid connect/disconnect cycles.",
|
|
"priority": "low",
|
|
"status": "identified-in-review",
|
|
"notes": "Also consider: periodic cleanup of game_conns sets for long-running games, rate limiting in auth layer"
|
|
}
|
|
],
|
|
|
|
"dependencies": {
|
|
"packages": {
|
|
"python-socketio": ">=5.10.0",
|
|
"aiohttp": "Required by python-socketio async mode"
|
|
},
|
|
"existingModules": [
|
|
"app.core.engine.GameEngine",
|
|
"app.core.visibility.get_visible_state",
|
|
"app.services.game_state_manager.GameStateManager",
|
|
"app.services.deck_service.DeckService",
|
|
"app.services.card_service.CardService",
|
|
"app.services.jwt_service.JWTService",
|
|
"app.db.models.game.ActiveGame",
|
|
"app.db.models.game.GameHistory"
|
|
]
|
|
},
|
|
|
|
"testStrategy": {
|
|
"unit": {
|
|
"location": "tests/unit/services/",
|
|
"scope": "GameService, ConnectionManager, TurnTimeoutService",
|
|
"mocking": "Mock GameEngine, GameStateManager, Redis"
|
|
},
|
|
"integration": {
|
|
"location": "tests/socketio/",
|
|
"scope": "Full WebSocket flow with real Redis/Postgres",
|
|
"tools": "testcontainers, python-socketio test client"
|
|
},
|
|
"e2e": {
|
|
"location": "tests/e2e/",
|
|
"scope": "Complete game from creation to completion",
|
|
"deferred": "May defer to after Phase 5"
|
|
}
|
|
},
|
|
|
|
"sequenceDiagrams": {
|
|
"gameCreation": [
|
|
"Client -> REST: POST /games {deck_id, rules_config}",
|
|
"REST -> GameService: create_game(user_id, deck_id, ...)",
|
|
"GameService -> DeckService: get_deck(deck_id)",
|
|
"GameService -> CardService: get_card_definitions()",
|
|
"GameService -> GameEngine: create_game(player_ids, decks, registry)",
|
|
"GameService -> GameStateManager: save_to_cache() + persist_to_db()",
|
|
"REST -> Client: {game_id, ws_url}",
|
|
"Client -> WebSocket: connect(jwt) + game:join(game_id)",
|
|
"WebSocket -> Client: game:state(visible_state)"
|
|
],
|
|
"actionExecution": [
|
|
"Client -> WebSocket: game:action({type: 'attack', attack_index: 0})",
|
|
"WebSocket -> GameService: execute_action(game_id, player_id, action)",
|
|
"GameService -> GameStateManager: load_state(game_id)",
|
|
"GameService -> GameEngine: execute_action(game, player_id, action)",
|
|
"GameService -> GameStateManager: save_to_cache()",
|
|
"GameService -> TurnTimeoutService: (re)start_timer if turn changed",
|
|
"WebSocket -> Room: game:action_result + game:state to each player"
|
|
],
|
|
"reconnection": [
|
|
"Client -> WebSocket: connect(jwt)",
|
|
"WebSocket -> ConnectionManager: get_user_active_game(user_id)",
|
|
"WebSocket -> GameService: resume_game(game_id, user_id)",
|
|
"GameService -> GameStateManager: load_state(game_id)",
|
|
"GameService -> TurnTimeoutService: extend_timer(grace_period)",
|
|
"WebSocket -> Client: game:state(visible_state, pending_actions)",
|
|
"WebSocket -> Opponent: game:opponent_connected"
|
|
]
|
|
}
|
|
}
|