26 KiB
MTG Online Multiplayer Game Server — Handoff Document
Project: MTG Online Web (Modern Web-based MTG Platform)
Previous Thread: Card Backend — Phase 1 & 2 (Deck Building, Card Import, User Data)
New Thread: Phase 3 — Multiplayer Game Server
Location: /home/wall-o/projects/mtgonline
Date: 2026-07-25
Context: What Was Built (Card Backend)
The previous thread delivered a complete card management and deckbuilding backend:
Completed Features
- FastAPI application with JWT authentication, dual PostgreSQL databases (
mtgonline+mtgdata), Redis caching - 29 REST endpoints across 10 routers (auth, users, decks, rooms, games, admin, cards, interactions, refresh, user-data, card-import)
- 16 user-data tables with Alembic migrations, CASCADE deletes, JSONB flexibility
- Card import feature with fuzzy matching for XLSX/CSV/JSON/ODS files
- Deck CRUD with DRAFT/FINAL status transitions, precedents, suggestions
- MTGJSON data pipeline downloading and upserting card data into
mtgdatadatabase - Frontend (React/TypeScript) with deck builder UI, card import, authentication, admin dashboard
Current Architecture
mtgonline/
├── backend/ # FastAPI card backend (Phase 1 & 2 complete)
│ ├── app/
│ │ ├── core/ # Settings, database engines, Redis client
│ │ ├── models/ # SQLAlchemy ORM models (user_data, user_deck, card_import)
│ │ ├── routers/ # API route modules (10 routers)
│ │ ├── schemas/ # Pydantic schemas
│ │ ├── services/ # MTGJSON manager, card DB, deck manager, fuzzy matcher
│ │ └── main.py # FastAPI app entry point
│ ├── alembic/ # Database migrations
│ └── Dockerfile
├── frontend/ # React/TypeScript deck builder (Phase 3 complete)
├── C++/ # Cockatrice architecture analysis (reference)
├── state.json # Current state tracking
└── ROADMAP.md # Full project roadmap
Key Integration Points Available
- Card lookup:
GET /api/cards/{card_id}— returns full card data for game display - Card search:
GET /api/cards/search?q=...— search cards during gameplay - Set lists:
GET /api/sets/— list available sets for game formatting - User decks:
GET /api/users/{user_id}/decks— retrieve FINAL decks for player selection - Card import status:
GET /api/v1/card-import/status— check if user has imported their collection - Database access: Direct SQLAlchemy async engines for
mtgonline(app data) andmtgdata(card data)
Phase 3: Multiplayer Game Server — Requirements
Objective
Build a real-time multiplayer game server that enables live MTG gameplay between multiple players via WebSocket connections. This is the core gameplay engine — the replacement for Cockatrice's servatrice component.
Success Criteria
- Players can create/join game rooms and play MTG in real-time
- Game state syncs correctly across all connected players (< 50ms latency)
- All 11 MTG turn phases are enforced server-side
- Stack resolution works correctly (layers, priority, targeting)
- Deck validation ensures legality before gameplay
- Game history is recorded as a serialized event log (for replays)
- Admin commands work (kick, ban, game control)
- Spectator mode is supported
- WebSocket reconnection handles gracefully with state sync
Architecture Blueprint (Derived from Cockatrice Analysis)
Authority Model
┌─────────────────┐ WebSocket (JSON) ┌─────────────────┐
│ CLIENT │◄────────────────────────────────────►│ SERVER │
│ (React/TS) │ │ (FastAPI/Py) │
│ │ │ │
│ • Game Scene │ GameCommands (play, attack, etc.) │ • PostgreSQL │
│ • Hand View │◄────────────────────────────────────►│ • Game State │
│ • Chat Panel │ GameEvents (state changes) │ • Room/Player │
│ • Deck Panel │ │ Management │
│ • Phase Toolbar │ Chat, Admin, Spectator │ • Replay Log │
└─────────────────┘ └─────────────────┘
Key Principle: Server is authoritative. Clients send commands, server validates and broadcasts events. No client-side state manipulation.
Game Engine Architecture
| Cockatrice Component | Modern Web Equivalent | Role |
|---|---|---|
AbstractGame |
Game (server-side) |
Core game instance holding state, players, event handler |
GameMetaInfo |
GameMetadata |
gameId, maxPlayers, description, started, spectators |
GameState |
GameBoardState |
currentPhase, activePlayer, hostId, gameTimer |
GameEventHandler |
GameEventDispatcher |
Central dispatch — processes events, prepares commands |
PlayerManager |
PlayerRegistry |
Coordinates all players in a game |
PlayerLogic |
Player |
Per-player game logic |
PlayerActions |
PlayerCommands |
Concrete commands: play, attack, tap, draw |
CardZone |
Zone (base class) |
Abstract zone — Hand/Stack/Table/Graveyard/Exile/Library |
HandZone |
Hand |
Player's hand (secret/hidden zone) |
StackZone |
Stack |
Spells/abilities on the stack |
TableZone |
Battlefield |
Permanents on the battlefield |
PileZone |
Pile (Graveyard, Exile, Library) |
Discard/exile/draw piles |
Replay |
GameReplay |
Serialized event stream for replay |
Phase |
TurnPhase |
11-phase MTG turn structure |
Turn Phase System (11 Phases with Sub-Phases)
Untap → Upkeep → Draw → Main 1 → Combat → Main 2 → End → Cleanup
│
└── Sub-phases:
Beginning of Combat
Declare Attackers
Declare Blockers
Combat Damage
End of Combat
Command/Event Flow
User Action (React component)
│
▼
GameCommand (JSON message)
│
▼
GameEventDispatcher.process()
│
▼
Player.handleCommand()
│
▼
ZoneLogic (state mutation)
│
▼
GameEvent (broadcast to all clients via WebSocket)
│
▼
Client receives & updates UI via Zustand/XState
Network Protocol Design
Cockatrice uses Protocol Buffers over TCP. Modern web equivalent:
| Cockatrice Proto Message | JSON WebSocket Message |
|---|---|
ServerInfo_Game |
{ "type": "game_info", "game_id": 1, "max_players": 2, ... } |
ServerInfo_Player |
{ "type": "player_info", "player_id": 1, "name": "...", ... } |
Command_PlayCard |
{ "type": "cmd_play_card", "card_id": 42, "zone": "hand", ... } |
Command_Attack |
{ "type": "cmd_attack", "attacker_id": 7, "targets": [3, 5], ... } |
Event_Join |
{ "type": "event_join", "player_id": 2, "properties": {...} } |
Event_Leave |
{ "type": "event_leave", "player_id": 1, "reason": "..." } |
Event_SetActivePlayer |
{ "type": "event_active_player", "player_id": 1 } |
Event_SetActivePhase |
{ "type": "event_active_phase", "phase": 5 } |
Event_GameSay |
{ "type": "event_chat", "player_id": 1, "message": "..." } |
GameReplay |
{ "type": "replay", "events": [...] } |
Architecture Patterns to Reuse
| Pattern | Cockatrice Usage | Modern Equivalent |
|---|---|---|
| Event Bus | Qt signals/slots | WebSocket broadcast + event system |
| Command Pattern | Command_PlayCard, Command_Attack |
JSON command messages, validated server-side |
| Observer | GameEventHandler emits signals |
WebSocket events trigger UI updates |
| Strategy | CardZoneLogic subclasses |
Zone classes with strategy pattern |
| Facade | AbstractGame |
Single Game object wrapping all subsystems |
| Memento | DeckListMemento for undo |
Immutable state snapshots for undo/redo |
| Repository | ServatriceDatabaseInterface |
SQLAlchemy repositories for game state |
Implementation Plan
Phase 3.1: Game Server Foundation
Deliverables
- Game Server Module — Separate FastAPI WebSocket endpoint for game rooms
- Game Room Management — Room creation, player join/leave, spectator support
- Game State Management — Server-authoritative game state with full phase tracking
- Player System — Player registration, connection management, disconnection handling
- WebSocket Protocol — JSON message protocol for commands and events
- Database Models — Game rooms, player connections, game state persistence
Files to Create
backend/app/
├── models/
│ ├── game_room.py # Game room model (room_id, game_type, host_id, status)
│ ├── game_player.py # Player connection (player_id, room_id, connection_id, hand, life)
│ ├── game_state.py # Game board state (phase, turn, cards, zones)
│ ├── game_event.py # Game events log (for replay)
│ └── game_log.py # Game history (serialized event log)
├── routers/
│ └── game_server.py # WebSocket endpoint for game rooms
├── services/
│ ├── game_engine.py # Core game engine (turn phases, combat, stack)
│ ├── game_state_manager.py # Game state management and synchronization
│ ├── player_manager.py # Player management (join, leave, disconnect)
│ ├── card_zone.py # Card zone logic (hand, stack, battlefield, pile)
│ ├── game_rule_engine.py # Game rule enforcement (mana, timing, targeting)
│ └── game_replay.py # Game replay recording and playback
├── schemas/
│ └── game_schemas.py # Pydantic schemas for game messages
└── ws/
├── connection.py # WebSocket connection management
└── message_handler.py # Message parsing and dispatch
Detailed Implementation Steps
Step 1: Game Room Model & Router
- Create
GameRoommodel with room_id, game_type, host_id, status (WAITING/PLAYING/FINISHED) - Create
GamePlayermodel with player_id, room_id, connection_id, hand (JSONB), life_total - Create
GameServerrouter with WebSocket endpoint/ws/game/{room_id} - Implement room creation (host creates room), player join/leave, spectator join/leave
- Handle WebSocket disconnection gracefully (player_timeout for reconnect)
Step 2: Game State Management
- Create
GameBoardStatemodel tracking: current_phase, active_player, turn_number, host_id - Track all zone contents: hands, stack, battlefield, graveyards, exiles, libraries
- Implement phase transitions (Untap → Upkeep → Draw → Main1 → Combat → Main2 → End → Cleanup)
- Handle sub-phases within Combat (Beginning, Declare Attackers, Declare Blockers, Combat Damage, End)
Step 3: Card Zone Logic
- Implement
CardZonebase class with common operations (add, remove, search, sort) - Implement
HandZone(secret zone, only visible to owner) - Implement
StackZone(priority-based, spell/ability resolution) - Implement
BattlefieldZone(permanent zone, battlefield effects) - Implement
PileZone(Graveyard, Exile, Library — shuffle, draw, discard) - Implement zone-specific behaviors (hand: count/mulligan, stack: layer resolution)
Step 4: Game Rule Engine
- Implement mana payment tracking and validation
- Implement timing rules (sorcery speed, instant speed, flash)
- Implement targeting validation (legal targets, priority chain)
- Implement combat rules (attackers, blockers, damage assignment, trample, first strike)
- Implement stack resolution (last in, first out, layer system)
- Implement life total tracking and loss conditions (0 life, poison, concession)
Step 5: Game Replay System
- Serialize all game events as a JSONB log
- Support replay recording (on game start) and playback (on request)
- Implement deterministic replay (same commands → same state)
- Store replays in
game_logstable with compressed JSONB
Step 6: Admin Commands
- Implement kick player command
- Implement ban player command (from mtgonline DB)
- Implement game pause/resume
- Implement spectator mode toggle
- Implement admin override for game state
Phase 3.2: Integration with Card Backend
Deliverables
- Deck Validation Service — Validate decks before game start
- Card Lookup Service — Fetch card data for game display
- User Authentication — JWT validation for game server
- Database Integration — Access to
mtgdataandmtgonlinedatabases
Integration Points
- Card Backend API (
http://backend:8000):GET /api/cards/{card_id}— Get card details for game board displayGET /api/cards/search?q=...— Search cards during gameplayGET /api/sets/— List available sets for game formattingGET /api/users/{user_id}/decks— Get user's FINAL decks for selection
- Direct PSQL Access (via shared connection string):
mtgdatadatabase — Read card data (cards, sets, etc.)mtgonlinedatabase — Read user decks (for deck validation, game setup)
Deck Validation Flow
Player selects deck → Server fetches deck from mtgonline DB
│
▼
Validate deck against rules:
│
├── Check card count (60+ for Standard/Modern, 100+ for Commander)
│ ├── Card validity (must exist in mtgdata)
│ ├── Color identity (Commander format)
│ └── Basic land count (max 4 of each non-basic)
│
├── Check banned cards (format-specific ban list)
│
└── Return validation result (pass/fail with reasons)
Phase 3.3: WebSocket Client Integration
Deliverables
- WebSocket Client — React component for game client
- State Management — Zustand store for game state
- Message Handlers — Parse and handle game events
- Reconnection Logic — Exponential backoff with state sync
Integration with Frontend
- WebSocket Connection: Connect to
/ws/game/{room_id}on game start - State Store: Use Zustand to manage game state, sync with server events
- Message Handlers: Handle join, leave, play_card, attack, etc.
- Reconnection: On disconnect, attempt reconnect with exponential backoff, sync state on reconnect
Database Schema for Game Server
Tables to Create
| Table | Purpose | Key Columns |
|---|---|---|
game_rooms |
Game room management | room_id (PK), game_type, host_id, status, created_at |
game_players |
Player connections | player_id (PK), room_id (FK), connection_id, hand (JSONB), life_total, is_spectator |
game_states |
Game board state | state_id (PK), room_id (FK), current_phase, turn_number, active_player, cards (JSONB), created_at |
game_events |
Event log for replay | event_id (PK), room_id (FK), event_type, event_data (JSONB), timestamp |
game_replays |
Recorded games | replay_id (PK), room_id (FK), events (JSONB), duration, started_at, finished_at |
JSONB Structures
Hand: {"cards": [{"card_id": 1, "name": "Lightning Bolt", "zone": "hand"}]}
Stack: {"items": [{"card_id": 5, "name": "Counterspell", "controller": 1}]}
Battlefield: {"cards": [{"card_id": 10, "name": "Craterhoof", "zone": "battlefield", "tapped": false}]}
Graveyard: {"cards": [{"card_id": 1, "name": "Lightning Bolt", "zone": "graveyard"}]}
Game Engine Implementation Details
Core Game Class
class Game:
"""Core game instance holding state, players, event handler."""
def __init__(self, room_id: str, game_type: str, host_id: int):
self.room_id = room_id
self.game_type = game_type # Standard, Modern, Commander
self.host_id = host_id
self.players: Dict[int, Player] = {}
self.state: GameBoardState = GameBoardState()
self.event_dispatcher: GameEventDispatcher = GameEventDispatcher(self)
self.replay: GameReplay = GameReplay()
def add_player(self, player: Player) -> bool:
"""Add player to game. Returns True if successful."""
def remove_player(self, player_id: int) -> bool:
"""Remove player from game. Broadcasts event."""
def transition_phase(self) -> None:
"""Advance to next phase. Validates phase transitions."""
def handle_command(self, command: GameCommand) -> List[GameEvent]:
"""Process player command. Returns events to broadcast."""
def broadcast_event(self, event: GameEvent) -> None:
"""Broadcast event to all connected clients."""
Player Class
class Player:
"""Per-player game logic."""
def __init__(self, player_id: int, name: str, deck: Deck):
self.player_id = player_id
self.name = name
self.deck = deck
self.hand: HandZone = HandZone()
self.battlefield: BattlefieldZone = BattlefieldZone()
self.graveyard: PileZone = PileZone()
self.exile: PileZone = PileZone()
self.library: LibraryZone = LibraryZone()
self.life_total: int = 20
self.poison_counters: int = 0
def handle_command(self, command: GameCommand) -> List[GameEvent]:
"""Process command. Returns events to broadcast."""
def play_card(self, card_id: int, zone: str) -> GameEvent:
"""Play card from hand to battlefield."""
def attack(self, attacker_id: int, targets: List[int]) -> GameEvent:
"""Declare attackers and assign damage."""
def tap_card(self, card_id: int) -> GameEvent:
"""Tap card for mana or attack."""
def draw_card(self) -> GameEvent:
"""Draw card from library."""
Zone Classes
class CardZone:
"""Abstract zone base class."""
def add_card(self, card: Card) -> None:
"""Add card to zone."""
def remove_card(self, card_id: int) -> Card:
"""Remove card from zone."""
def search(self, filter: SearchFilter) -> List[Card]:
"""Search zone with filter."""
def shuffle(self) -> None:
"""Shuffle zone."""
def sort(self) -> None:
"""Sort zone."""
class HandZone(CardZone):
"""Player's hand (secret zone)."""
# Only visible to owner
class StackZone(CardZone):
"""Spells/abilities on the stack."""
# Priority-based resolution
class BattlefieldZone(CardZone):
"""Permanent zone."""
# Has tap/untap, counters, attachments
class PileZone(CardZone):
"""Graveyard, exile, library."""
# Shuffleable (library) or non-shuffleable (graveyard)
Testing Strategy
Unit Tests
- Game State Transitions: Test all 11 phases and sub-phases
- Combat Rules: Test attack, block, damage assignment, trample, first strike
- Stack Resolution: Test priority, layer system, LIFO resolution
- Mana Payment: Test mana pool, timing, costs
- Zone Operations: Test add/remove/search/shuffle/sort for each zone
- Deck Validation: Test format rules (Standard, Modern, Commander)
- Replay Determinism: Test same commands → same state
Integration Tests
- WebSocket Connection: Test connect/disconnect/join/leave
- Game Room Management: Test room creation, player count, spectator
- Real-time Sync: Test state sync across multiple clients
- Reconnection: Test reconnect with state sync
- Admin Commands: Test kick, ban, pause, resume
Load Tests
- Concurrent Players: Test 2-8 players per game
- Multiple Games: Test 100+ concurrent games
- Message Throughput: Test 1000+ messages/sec per game
- Latency: Test < 50ms message latency
Acceptance Criteria
Functional Requirements
- Players can create game rooms with custom settings
- Players can join existing game rooms
- Game starts with all players ready
- 11-phase turn structure works correctly
- Combat rules enforce attack/block/damage correctly
- Stack resolves spells/abilities in correct order
- Mana payment validates costs and timing
- Deck validation ensures format legality
- Game history recorded as serialized event log
- Admin can kick/ban players
- Spectators can watch games
- WebSocket disconnect triggers graceful player timeout
Performance Requirements
- WebSocket latency < 50ms for game state updates
- Support 1000+ concurrent users across multiple games
- Game state syncs within 100ms across all connected players
- Replay recording doesn't impact gameplay performance
- Database queries for card lookup < 10ms
Security Requirements
- JWT authentication for all WebSocket connections
- Input validation for all game commands
- Rate limiting for message spam protection
- Admin-only commands require admin role
- Game state never exposed to spectators (unless enabled)
Files to Create/Modify
New Files
backend/app/
├── models/
│ ├── game_room.py
│ ├── game_player.py
│ ├── game_state.py
│ ├── game_event.py
│ └── game_log.py
├── routers/
│ └── game_server.py
├── services/
│ ├── game_engine.py
│ ├── game_state_manager.py
│ ├── player_manager.py
│ ├── card_zone.py
│ ├── game_rule_engine.py
│ └── game_replay.py
├── schemas/
│ └── game_schemas.py
└── ws/
├── connection.py
└── message_handler.py
Modified Files
backend/app/
├── main.py # Mount game server router
├── core/
│ ├── database.py # Add game database engine if needed
│ └── settings.py # Add game server settings
├── alembic/
│ └── versions/
│ └── 005_game_server_tables.py
Dependencies to Add
# backend/requirements.txt
websockets>=12.0
pydantic>=2.9.2
Integration with Card Backend
API Endpoints Used by Game Server
| Endpoint | Purpose | Method |
|---|---|---|
/api/cards/{card_id} |
Get card details | GET |
/api/cards/search?q=... |
Search cards | GET |
/api/sets/ |
List sets | GET |
/api/users/{user_id}/decks |
Get user decks | GET |
/api/v1/card-import/status |
Check import status | GET |
Database Access
mtgdata database (read-only):
mtg_cards— Card data for game displaymtg_sets— Set data for format rulesmtg_card_types— Card type definitions
mtgonline database (read-write):
users— User authenticationdecks— User decks for validationban— Ban list for admin commandsgame_logs— Game history storage
Cockatrice Analysis Reference
The complete architecture analysis is at /home/wall-o/projects/mtgonline/C++/ARCHITECTURE_ANALYSIS.md.
Key takeaways:
- Server authoritative: Never trust client state
- Deterministic replay: Same commands → same state
- Zone abstraction: Each zone independently implementable
- Phase system: 11-phase MTG turn with sub-phases
- Command/Event separation: What player wants vs. what happens
The modern web adaptation replaces:
- TCP with WebSocket (JSON)
- Protocol Buffers with JSON messages
- Qt Widgets with React/Next.js
- C++/MySQL with Python/FastAPI + PostgreSQL
- Monolithic handler with modular service architecture
Next Steps
- Start with Phase 3.1: Game server foundation (models, router, WebSocket)
- Implement core game engine: Turn phases, combat, stack resolution
- Add zone logic: Hand, stack, battlefield, pile zones
- Integrate with card backend: Deck validation, card lookup
- Add replay system: Serialize events for replay
- Test thoroughly: Unit, integration, load tests
- Update documentation: API docs, architecture docs
- Push to Gitea: Commit and push all changes
Quick Reference
Game Room WebSocket Endpoint
WS /ws/game/{room_id}
Command Messages
{
"type": "cmd_play_card",
"card_id": 42,
"zone": "hand",
"targets": [3, 5],
"mana_cost": [1, "R", "G"]
}
Event Messages
{
"type": "event_play_card",
"card_id": 42,
"controller": 1,
"zone": "battlefield",
"timestamp": 1784948189
}
State Sync Message
{
"type": "state_sync",
"game_state": {
"current_phase": "main1",
"turn_number": 5,
"active_player": 1,
"hands": {"1": [...], "2": [...]},
"battlefield": [...],
"stack": [...],
"graveyards": {"1": [...], "2": [...]}
}
}
Environment Variables
Add to .env or docker-compose:
# Game Server
GAME_SERVER_HOST=0.0.0.0
GAME_SERVER_PORT=8765
GAME_WS_PATH=/ws/game
GAME_TOKEN_EXPIRY=3600
GAME_PLAYER_TIMEOUT=30
GAME_MAX_PLAYERS=8
Docker Configuration
Add to docker-compose.dev.yml:
game-server:
build:
context: ./backend
dockerfile: Dockerfile
ports:
- "8765:8765"
environment:
- DATABASE_URL=${DATABASE_URL}
- MTG_DATABASE_URL=${MTG_DATABASE_URL}
- REDIS_URL=${REDIS_URL}
- GAME_SERVER_PORT=8765
- GAME_WS_PATH=/ws/game
depends_on:
- postgres
- mtgdata
- redis
networks:
- mtgonline
Last Updated: 2026-07-25
Status: Handoff complete. Ready for Phase 3 implementation.
Next Action: Begin Phase 3.1 — Game server foundation (models, router, WebSocket).