# WebSocket Game Server Architecture ## Overview The MTG Online WebSocket game server provides real-time game state synchronization and player action handling for multiplayer Magic: The Gathering sessions. This document defines the architecture, protocol, and message formats. ## Architecture ``` ┌─────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Player 1 │────▶│ WebSocket │────▶│ Game Server │ │ Browser │◀────│ Connection │◀────│ (asyncio) │ └─────────────┘ └─────────────────┘ └────────┬────────┘ │ ┌─────────────┐ ┌─────────────────┐ ┌────────┴────────┐ │ Player 2 │────▶│ WebSocket │────▶│ Game State │ │ Browser │◀────│ Connection │◀────│ Manager │ └─────────────┘ └─────────────────┘ └────────┬────────┘ │ ┌─────────────┐ ┌─────────────────┐ ┌────────┴────────┐ │ Player 3 │────▶│ WebSocket │────▶│ PostgreSQL │ │ Browser │◀────│ Connection │◀────│ (Persistence) │ └─────────────┘ └─────────────────┘ └─────────────────┘ ``` ### Components 1. **WebSocket Endpoint** (`/ws/game/{game_id}`) - Accepts connections - Authenticates players - Routes messages to/from game instances 2. **Game Server Manager** - Manages active game instances - Handles game lifecycle (create, join, start, end) - Coordinates between players 3. **Game State Manager** - Maintains authoritative game state - Validates player actions - Emits state changes to all players 4. **Persistence Layer** - Saves game state to PostgreSQL - Enables game replay and recovery ## Protocol Design ### Message Format All messages use JSON with the following envelope: ```json { "type": "message_type", "seq": 123, "ts": 1697750000.123, "data": { // Message-specific payload } } ``` - `type`: Message type identifier - `seq`: Sequence number for ordering and gap detection - `ts`: Unix timestamp (milliseconds) - `data`: Message-specific payload ### Message Types #### Connection Management | Type | Direction | Description | |------|-----------|-------------| | `hello` | Client → Server | Initial connection with auth token | | `welcome` | Server → Client | Connection established, game info | | `bye` | Client → Server | Player disconnecting | | `ping` | Either | Keepalive | | `pong` | Either | Keepalive response | #### Game Lifecycle | Type | Direction | Description | |------|-----------|-------------| | `game_start` | Server → Client | Game starting, initial state | | `game_state` | Server → Client | Full game state snapshot | | `state_update` | Server → Client | Incremental state change | | `game_over` | Server → Client | Game ended with results | #### Player Actions | Type | Direction | Description | |------|-----------|-------------| | `action` | Client → Server | Player takes an action | | `action_ack` | Server → Client | Action acknowledged | | `action_error` | Server → Client | Action failed validation | #### Turn Management | Type | Direction | Description | |------|-----------|-------------| | `turn_start` | Server → Client | New turn beginning | | `phase_change` | Server → Client | Phase transition | | `priority` | Server → Client | Player has priority | | `stack_empty` | Server → Client | Stack is empty | #### Chat | Type | Direction | Description | |------|-----------|-------------| | `chat` | Either | Chat message | | `chat_history` | Server → Client | Chat history on join | ### Authentication Players authenticate via JWT token in the `hello` message: ```json { "type": "hello", "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "game_id": 42, "seat": 1 } } ``` ## Game State Model ### Full State Snapshot ```json { "game_id": 42, "room_id": 5, "game_type": "Commander", "state": "IN_PROGRESS", "turn": 3, "phase": "main1", "players": [ { "player_id": 1, "user_id": 101, "seat": 1, "life_total": 18, "hand_size": 5, "library_size": 95, "graveyard_size": 3, "is_active": true, "has_priority": true }, { "player_id": 2, "user_id": 102, "seat": 2, "life_total": 20, "hand_size": 6, "library_size": 94, "graveyard_size": 2, "is_active": false, "has_priority": false } ], "battlefield": { "player_1": [ { "card_id": "card_123", "name": "Lightning Bolt", "type": "Creature", "power": 2, "toughness": 2, "is_tapped": false, "is_attacking": false, "damage": 0 } ], "player_2": [] }, "stack": [], "graveyard": { "player_1": [], "player_2": [] } } ``` ### Incremental State Update ```json { "type": "state_update", "seq": 124, "data": { "changes": [ { "player_id": 1, "field": "life_total", "old_value": 20, "new_value": 18 }, { "player_id": 1, "field": "hand_size", "old_value": 6, "new_value": 5 } ], "reason": "played_lightning_bolt" } } ``` ## Player Action Protocol ### Action Submission ```json { "type": "action", "seq": 125, "data": { "action_type": "play_card", "card_id": "card_456", "target": null, "mode": "attack" } } ``` ### Action Types | Action Type | Description | Required Fields | |-------------|-------------|-----------------| | `play_card` | Play a card from hand | `card_id`, `target` (optional) | | `tap_card` | Tap a card | `card_id` | | `untap_card` | Untap a card | `card_id` | | `attack` | Declare attacker | `card_id`, `target_player` | | `block` | Declare blocker | `card_id`, `target_card_id` | | `assign_damage` | Assign combat damage | `card_id`, `target_card_id`, `damage` | | `pass_priority` | Pass priority | - | | `cast_spell` | Cast a spell | `card_id`, `target` (optional) | | `activate_ability` | Activate ability | `card_id`, `ability_id`, `target` (optional) | | `end_turn` | End current turn | - | | `concede` | Concede the game | - | ### Action Validation The server validates all actions against the current game state: 1. **Turn/Phase Check**: Is this action legal in the current phase? 2. **Priority Check**: Does the player have priority? 3. **Card State Check**: Is the card in the correct zone? 4. **Target Check**: Is the target valid? 5. **Cost Check**: Can the player pay the cost? ## Turn and Phase Management ### Turn Structure ``` Turn Start └─ Untap Phase └─ Untap all permanents └─ Upkeep Phase └─ Upkeep costs └─ Draw Phase └─ Draw a card └─ Main Phase 1 └─ Play lands, creatures, instants └─ Combat Phase └─ Beginning of Combat └─ Declare Attackers └─ Declare Blockers └─ Combat Damage └─ End of Combat └─ Main Phase 2 └─ Play lands, creatures, instants └─ End Phase └─ Cleanup Turn End ``` ### Priority System The server tracks priority and only accepts actions from the player with priority: ```json { "type": "priority", "data": { "player_id": 1, "phase": "main1", "stack_size": 0 } } ``` ## Error Handling ### Action Errors ```json { "type": "action_error", "seq": 126, "data": { "action_seq": 125, "error_code": "INVALID_TARGET", "error_message": "Target player is not in the game", "action_type": "play_card" } } ``` ### Error Codes | Code | Description | |------|-------------| | `INVALID_ACTION` | Action not legal in current phase | | `NO_PRIORITY` | Player does not have priority | | `INVALID_TARGET` | Target is not valid | | `CARD_NOT_IN_HAND` | Card not in player's hand | | `CARD_NOT_ON_BATTLEFIELD` | Card not on battlefield | | `CANT_PAY_COST` | Player cannot pay the cost | | `STACK_NOT_EMPTY` | Stack is not empty | | `GAME_OVER` | Game is already over | ## Reconnection and Recovery ### Reconnection Protocol When a player reconnects: 1. Client sends `hello` with same game_id and seat 2. Server validates player is still in the game 3. Server sends `welcome` with current game state 4. Server sends `chat_history` for the session 5. Player resumes play ### State Recovery If the server crashes: 1. Game state is loaded from PostgreSQL 2. Players are notified to reconnect 3. Game resumes from last saved state ## Concurrency Model ### asyncio-based Architecture ```python class GameServer: def __init__(self): self.games: Dict[int, GameInstance] = {} self.player_connections: Dict[int, WebSocket] = {} async def handle_connection(self, websocket, game_id): """Handle a single WebSocket connection.""" pass async def game_loop(self, game_id): """Main game loop for a game instance.""" pass class GameInstance: def __init__(self, game_id, players): self.game_id = game_id self.players = players self.state = GameState() self.lock = asyncio.Lock() async def process_action(self, player_id, action): """Process a player action with locking.""" async with self.lock: return self.state.apply_action(player_id, action) ``` ### Locking Strategy - **Game-level lock**: Protects game state mutations - **Action queue**: Orders actions within a turn - **Phase transitions**: Synchronized across all players ## Performance Considerations ### Message Size - Full state snapshots: ~5-10 KB - Incremental updates: ~100-500 bytes - Chat messages: ~50-200 bytes ### Latency Targets - Action acknowledgment: < 100ms - State update delivery: < 200ms - Turn transition: < 500ms ### Scaling - Single server: ~50 concurrent games - Multiple servers: Use Redis pub/sub for cross-server communication - Database: PostgreSQL for persistence, Redis for caching ## Security ### Message Validation - All messages validated against schema - Sequence numbers prevent replay attacks - JWT tokens expire after 1 hour - Rate limiting on actions per second ### Data Integrity - Server is authoritative - Client cannot modify game state directly - All actions validated before application ## Implementation Plan ### Phase 1: Basic Infrastructure - [ ] WebSocket endpoint setup - [ ] Connection management - [ ] Authentication - [ ] Basic message routing ### Phase 2: Game State Management - [ ] Game state model - [ ] State serialization - [ ] Full state snapshots - [ ] Incremental updates ### Phase 3: Turn and Phase Management - [ ] Turn structure implementation - [ ] Phase transitions - [ ] Priority system - [ ] Action validation ### Phase 4: Player Actions - [ ] Card play actions - [ ] Combat actions - [ ] Spell casting - [ ] Ability activation ### Phase 5: Persistence and Recovery - [ ] Game state persistence - [ ] Reconnection handling - [ ] Game recovery - [ ] Chat history ### Phase 6: Testing and Optimization - [ ] Unit tests - [ ] Integration tests - [ ] Load testing - [ ] Performance optimization ## References - [FastAPI WebSocket Documentation](https://fastapi.tiangolo.com/advanced/websockets/) - [Magic: The Gathering Comprehensive Rules](https://magic.wizards.com/en/rules) - [WebSocket Protocol RFC 6455](https://tools.ietf.org/html/rfc6455)