Add comprehensive project documentation and Docker infrastructure for Paper Dynasty Real-Time Game Engine - a web-based multiplayer baseball simulation platform replacing the legacy Google Sheets system. Documentation Added: - Complete PRD (Product Requirements Document) - Project README with dual development workflows - Implementation guide with 5-phase roadmap - Architecture docs (backend, frontend, database, WebSocket) - CLAUDE.md context files for each major directory Infrastructure Added: - Root docker-compose.yml for full stack orchestration - Dockerfiles for backend and both frontends (multi-stage builds) - .dockerignore files for optimal build context - .env.example with all required configuration - Updated .gitignore for Python, Node, Nuxt, and Docker Project Structure: - backend/ - FastAPI + Socket.io game engine (Python 3.11+) - frontend-sba/ - SBA League Nuxt 3 frontend - frontend-pd/ - PD League Nuxt 3 frontend - .claude/implementation/ - Detailed implementation guides Supports two development workflows: 1. Local dev (recommended): Services run natively with hot-reload 2. Full Docker: One-command stack orchestration for testing/demos Next: Phase 1 implementation (backend/frontend foundations)
12 KiB
WebSocket Protocol Specification
Overview
Real-time bidirectional communication protocol between game clients and backend server using Socket.io. All game state updates, player actions, and system events transmitted via WebSocket.
Connection Lifecycle
1. Initial Connection
Client → Server
import { io } from 'socket.io-client'
const socket = io('wss://api.paperdynasty.com', {
auth: {
token: 'jwt-token-here'
},
reconnection: true,
reconnectionDelay: 1000,
reconnectionAttempts: 5
})
Server → Client
{
"event": "connected",
"data": {
"user_id": "123456789",
"connection_id": "abc123xyz"
}
}
2. Joining a Game
Client → Server
{
"event": "join_game",
"data": {
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"role": "player" // "player" or "spectator"
}
}
Server → Client (Success)
{
"event": "game_joined",
"data": {
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"role": "player",
"team_id": 42
}
}
Server → All Participants (User Joined Notification)
{
"event": "user_connected",
"data": {
"user_id": "123456789",
"role": "player",
"team_id": 42,
"timestamp": "2025-10-21T19:45:23Z"
}
}
3. Receiving Game State
Server → Client (Full State on Join)
{
"event": "game_state_update",
"data": {
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "active",
"inning": 3,
"half": "top",
"outs": 2,
"balls": 2,
"strikes": 1,
"home_score": 2,
"away_score": 1,
"runners": {
"first": null,
"second": 12345,
"third": null
},
"current_batter": {
"card_id": 67890,
"player_id": 999,
"name": "Mike Trout",
"position": "CF",
"batting_avg": 0.305,
"image": "https://..."
},
"current_pitcher": {
"card_id": 11111,
"player_id": 888,
"name": "Sandy Alcantara",
"position": "P",
"era": 2.45,
"image": "https://..."
},
"decision_required": {
"type": "set_defense",
"team_id": 42,
"user_id": "123456789",
"timeout_seconds": 30
},
"play_history": [
{
"play_number": 44,
"inning": 3,
"description": "Groundout to second base",
"runs_scored": 0
}
]
}
}
4. Heartbeat
Client → Server (Every 30 seconds)
{
"event": "heartbeat"
}
Server → Client
{
"event": "heartbeat_ack",
"data": {
"timestamp": "2025-10-21T19:45:23Z"
}
}
5. Disconnection
Client → Server
{
"event": "leave_game",
"data": {
"game_id": "550e8400-e29b-41d4-a716-446655440000"
}
}
Server → All Participants
{
"event": "user_disconnected",
"data": {
"user_id": "123456789",
"timestamp": "2025-10-21T19:46:00Z"
}
}
Game Action Events
1. Set Defensive Positioning
Client → Server
{
"event": "set_defense",
"data": {
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"positioning": "standard" // "standard", "infield_in", "shift_left", "shift_right"
}
}
Server → Client (Acknowledgment)
{
"event": "decision_recorded",
"data": {
"type": "set_defense",
"positioning": "standard",
"timestamp": "2025-10-21T19:45:25Z"
}
}
Server → All Participants (Next Decision Required)
{
"event": "decision_required",
"data": {
"type": "set_stolen_base",
"team_id": 42,
"user_id": "123456789",
"runners": ["second"],
"timeout_seconds": 20
}
}
2. Stolen Base Attempt
Client → Server
{
"event": "set_stolen_base",
"data": {
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"attempts": {
"second": true, // Runner on second attempts
"third": false // No runner on third
}
}
}
3. Offensive Approach
Client → Server
{
"event": "set_offensive_approach",
"data": {
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"approach": "swing_away" // "swing_away", "bunt", "hit_and_run"
}
}
4. Dice Roll & Play Resolution
Server → All Participants (Dice Roll Animation)
{
"event": "dice_rolled",
"data": {
"roll": 14,
"animation_duration": 2000,
"timestamp": "2025-10-21T19:45:30Z"
}
}
Server → All Participants (Play Outcome)
{
"event": "play_completed",
"data": {
"play_number": 45,
"inning": 3,
"half": "top",
"dice_roll": 14,
"result_type": "single",
"hit_location": "left_field",
"description": "Mike Trout singles to left field. Runner advances to third.",
"batter": {
"card_id": 67890,
"name": "Mike Trout"
},
"pitcher": {
"card_id": 11111,
"name": "Sandy Alcantara"
},
"outs_before": 2,
"outs_recorded": 0,
"outs_after": 2,
"runners_before": {
"first": null,
"second": 12345,
"third": null
},
"runners_after": {
"first": 67890,
"second": null,
"third": 12345
},
"runs_scored": 0,
"home_score": 2,
"away_score": 1,
"timestamp": "2025-10-21T19:45:32Z"
}
}
5. Play Result Selection
When multiple outcomes possible (e.g., hit location choices):
Server → Offensive Player
{
"event": "select_play_result",
"data": {
"play_number": 45,
"options": [
{
"value": "single_left",
"label": "Single to Left",
"description": "Runner advances to third"
},
{
"value": "single_center",
"label": "Single to Center",
"description": "Runner scores"
}
],
"timeout_seconds": 15
}
}
Client → Server
{
"event": "select_play_result",
"data": {
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"play_number": 45,
"selection": "single_center"
}
}
6. Substitution
Client → Server
{
"event": "substitute_player",
"data": {
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"card_out": 67890,
"card_in": 55555,
"position": "CF",
"batting_order": 3
}
}
Server → All Participants
{
"event": "substitution_made",
"data": {
"team_id": 42,
"player_out": {
"card_id": 67890,
"name": "Mike Trout"
},
"player_in": {
"card_id": 55555,
"name": "Byron Buxton",
"position": "CF",
"batting_order": 3
},
"inning": 3,
"timestamp": "2025-10-21T19:46:00Z"
}
}
7. Pitching Change
Client → Server
{
"event": "change_pitcher",
"data": {
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"pitcher_out": 11111,
"pitcher_in": 22222
}
}
Server → All Participants
{
"event": "pitcher_changed",
"data": {
"team_id": 42,
"pitcher_out": {
"card_id": 11111,
"name": "Sandy Alcantara",
"final_line": "6 IP, 4 H, 1 R, 1 ER, 2 BB, 8 K"
},
"pitcher_in": {
"card_id": 22222,
"name": "Edwin Diaz",
"position": "P"
},
"inning": 7,
"timestamp": "2025-10-21T19:47:00Z"
}
}
System Events
1. Inning Change
Server → All Participants
{
"event": "inning_change",
"data": {
"inning": 4,
"half": "top",
"home_score": 2,
"away_score": 1,
"timestamp": "2025-10-21T19:48:00Z"
}
}
2. Game Ended
Server → All Participants
{
"event": "game_ended",
"data": {
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"winner_team_id": 42,
"final_score": {
"home": 5,
"away": 3
},
"innings": 9,
"duration_minutes": 87,
"mvp": {
"card_id": 67890,
"name": "Mike Trout",
"stats": "3-4, 2 R, 2 RBI, HR"
},
"completed_at": "2025-10-21T20:15:00Z"
}
}
3. Decision Timeout Warning
Server → User
{
"event": "decision_timeout_warning",
"data": {
"decision_type": "set_defense",
"seconds_remaining": 10,
"default_action": "standard"
}
}
4. Auto-Decision Made
Server → All Participants
{
"event": "auto_decision",
"data": {
"decision_type": "set_defense",
"team_id": 42,
"action_taken": "standard",
"reason": "timeout",
"timestamp": "2025-10-21T19:45:55Z"
}
}
Error Events
1. Invalid Action
Server → Client
{
"event": "invalid_action",
"data": {
"action": "set_defense",
"reason": "Not your turn",
"current_decision": {
"type": "set_offense",
"team_id": 99
},
"timestamp": "2025-10-21T19:46:00Z"
}
}
2. Connection Error
Server → Client
{
"event": "connection_error",
"data": {
"code": "AUTH_FAILED",
"message": "Invalid or expired token",
"reconnect": false
}
}
3. Game Error
Server → All Participants
{
"event": "game_error",
"data": {
"code": "STATE_RECOVERY_FAILED",
"message": "Unable to recover game state",
"severity": "critical",
"recovery_options": [
"reload_game",
"contact_support"
],
"timestamp": "2025-10-21T19:46:30Z"
}
}
Rate Limiting
Per-User Limits
- Actions: 10 per second
- Heartbeats: 1 per 10 seconds minimum
- Invalid actions: 5 per minute (after that, temporary ban)
Response on Rate Limit
{
"event": "rate_limit_exceeded",
"data": {
"action": "set_defense",
"limit": 10,
"window": "1 second",
"retry_after": 1000,
"timestamp": "2025-10-21T19:46:35Z"
}
}
Reconnection Protocol
Automatic Reconnection
- Client detects disconnect
- Client attempts reconnect with backoff (1s, 2s, 4s, 8s, 16s)
- On successful reconnect, client sends
join_gameevent - Server checks if game state exists in memory
- If not, server recovers state from database
- Server sends full
game_state_updateto client - Client resumes from current state
Client Implementation
socket.on('disconnect', () => {
console.log('Disconnected, will attempt reconnect')
// Socket.io handles reconnection automatically
})
socket.on('connect', () => {
console.log('Reconnected')
// Rejoin game room
socket.emit('join_game', { game_id: currentGameId, role: 'player' })
})
Testing WebSocket Events
Using Python Client
import socketio
sio = socketio.Client()
@sio.event
def connect():
print('Connected')
sio.emit('join_game', {'game_id': 'test-game', 'role': 'player'})
@sio.event
def game_state_update(data):
print(f'Game state: {data}')
sio.connect('http://localhost:8000', auth={'token': 'jwt-token'})
sio.wait()
Using Browser Console
const socket = io('http://localhost:8000', {
auth: { token: 'jwt-token' }
})
socket.on('connect', () => {
console.log('Connected')
socket.emit('join_game', { game_id: 'test-game', role: 'player' })
})
socket.on('game_state_update', (data) => {
console.log('Game state:', data)
})
Event Flow Diagrams
Typical At-Bat Flow
1. [Server → All] decision_required (set_defense)
2. [Client → Server] set_defense
3. [Server → All] decision_recorded
4. [Server → All] decision_required (set_stolen_base) [if runners on base]
5. [Client → Server] set_stolen_base
6. [Server → All] decision_recorded
7. [Server → All] decision_required (set_offensive_approach)
8. [Client → Server] set_offensive_approach
9. [Server → All] decision_recorded
10. [Server → All] dice_rolled
11. [Server → All] play_completed
12. [Server → All] game_state_update
13. Loop to step 1 for next at-bat
Substitution Flow
1. [Client → Server] substitute_player
2. [Server validates]
3. [Server → All] substitution_made
4. [Server → All] game_state_update (with new lineup)
Security Considerations
Authentication
- JWT token required for initial connection
- Token verified on every connection attempt
- Token refresh handled by HTTP API, not WebSocket
Authorization
- User can only perform actions for their team
- Spectators receive read-only events
- Server validates all actions against game rules
Data Validation
- All incoming events validated against Pydantic schemas
- Invalid events logged and rejected
- Repeated invalid events result in disconnect
Implementation: See backend-architecture.md for connection manager implementation.