Files
mtgonline/handoff.md
T

704 lines
26 KiB
Markdown

# 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 `mtgdata` database
- **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) and `mtgdata` (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
1. **Game Server Module** — Separate FastAPI WebSocket endpoint for game rooms
2. **Game Room Management** — Room creation, player join/leave, spectator support
3. **Game State Management** — Server-authoritative game state with full phase tracking
4. **Player System** — Player registration, connection management, disconnection handling
5. **WebSocket Protocol** — JSON message protocol for commands and events
6. **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 `GameRoom` model with room_id, game_type, host_id, status (WAITING/PLAYING/FINISHED)
- Create `GamePlayer` model with player_id, room_id, connection_id, hand (JSONB), life_total
- Create `GameServer` router 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 `GameBoardState` model 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 `CardZone` base 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_logs` table 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
1. **Deck Validation Service** — Validate decks before game start
2. **Card Lookup Service** — Fetch card data for game display
3. **User Authentication** — JWT validation for game server
4. **Database Integration** — Access to `mtgdata` and `mtgonline` databases
#### Integration Points
- **Card Backend API** (`http://backend:8000`):
- `GET /api/cards/{card_id}` — Get card details for game board display
- `GET /api/cards/search?q=...` — Search cards during gameplay
- `GET /api/sets/` — List available sets for game formatting
- `GET /api/users/{user_id}/decks` — Get user's FINAL decks for selection
- **Direct PSQL Access** (via shared connection string):
- `mtgdata` database — Read card data (cards, sets, etc.)
- `mtgonline` database — 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
1. **WebSocket Client** — React component for game client
2. **State Management** — Zustand store for game state
3. **Message Handlers** — Parse and handle game events
4. **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
```python
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
```python
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
```python
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 display
- `mtg_sets` — Set data for format rules
- `mtg_card_types` — Card type definitions
**mtgonline database** (read-write):
- `users` — User authentication
- `decks` — User decks for validation
- `ban` — Ban list for admin commands
- `game_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
1. **Start with Phase 3.1**: Game server foundation (models, router, WebSocket)
2. **Implement core game engine**: Turn phases, combat, stack resolution
3. **Add zone logic**: Hand, stack, battlefield, pile zones
4. **Integrate with card backend**: Deck validation, card lookup
5. **Add replay system**: Serialize events for replay
6. **Test thoroughly**: Unit, integration, load tests
7. **Update documentation**: API docs, architecture docs
8. **Push to Gitea**: Commit and push all changes
---
## Quick Reference
### Game Room WebSocket Endpoint
```
WS /ws/game/{room_id}
```
### Command Messages
```json
{
"type": "cmd_play_card",
"card_id": 42,
"zone": "hand",
"targets": [3, 5],
"mana_cost": [1, "R", "G"]
}
```
### Event Messages
```json
{
"type": "event_play_card",
"card_id": 42,
"controller": 1,
"zone": "battlefield",
"timestamp": 1784948189
}
```
### State Sync Message
```json
{
"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:
```bash
# 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`:
```yaml
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).