12 KiB
12 KiB
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
-
WebSocket Endpoint (
/ws/game/{game_id})- Accepts connections
- Authenticates players
- Routes messages to/from game instances
-
Game Server Manager
- Manages active game instances
- Handles game lifecycle (create, join, start, end)
- Coordinates between players
-
Game State Manager
- Maintains authoritative game state
- Validates player actions
- Emits state changes to all players
-
Persistence Layer
- Saves game state to PostgreSQL
- Enables game replay and recovery
Protocol Design
Message Format
All messages use JSON with the following envelope:
{
"type": "message_type",
"seq": 123,
"ts": 1697750000.123,
"data": {
// Message-specific payload
}
}
type: Message type identifierseq: Sequence number for ordering and gap detectionts: 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:
{
"type": "hello",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"game_id": 42,
"seat": 1
}
}
Game State Model
Full State Snapshot
{
"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
{
"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
{
"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:
- Turn/Phase Check: Is this action legal in the current phase?
- Priority Check: Does the player have priority?
- Card State Check: Is the card in the correct zone?
- Target Check: Is the target valid?
- 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:
{
"type": "priority",
"data": {
"player_id": 1,
"phase": "main1",
"stack_size": 0
}
}
Error Handling
Action Errors
{
"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:
- Client sends
hellowith same game_id and seat - Server validates player is still in the game
- Server sends
welcomewith current game state - Server sends
chat_historyfor the session - Player resumes play
State Recovery
If the server crashes:
- Game state is loaded from PostgreSQL
- Players are notified to reconnect
- Game resumes from last saved state
Concurrency Model
asyncio-based Architecture
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