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)
668 lines
12 KiB
Markdown
668 lines
12 KiB
Markdown
# 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**
|
|
```typescript
|
|
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**
|
|
```json
|
|
{
|
|
"event": "connected",
|
|
"data": {
|
|
"user_id": "123456789",
|
|
"connection_id": "abc123xyz"
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. Joining a Game
|
|
|
|
**Client → Server**
|
|
```json
|
|
{
|
|
"event": "join_game",
|
|
"data": {
|
|
"game_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"role": "player" // "player" or "spectator"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Server → Client** (Success)
|
|
```json
|
|
{
|
|
"event": "game_joined",
|
|
"data": {
|
|
"game_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"role": "player",
|
|
"team_id": 42
|
|
}
|
|
}
|
|
```
|
|
|
|
**Server → All Participants** (User Joined Notification)
|
|
```json
|
|
{
|
|
"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)
|
|
```json
|
|
{
|
|
"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)
|
|
```json
|
|
{
|
|
"event": "heartbeat"
|
|
}
|
|
```
|
|
|
|
**Server → Client**
|
|
```json
|
|
{
|
|
"event": "heartbeat_ack",
|
|
"data": {
|
|
"timestamp": "2025-10-21T19:45:23Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
### 5. Disconnection
|
|
|
|
**Client → Server**
|
|
```json
|
|
{
|
|
"event": "leave_game",
|
|
"data": {
|
|
"game_id": "550e8400-e29b-41d4-a716-446655440000"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Server → All Participants**
|
|
```json
|
|
{
|
|
"event": "user_disconnected",
|
|
"data": {
|
|
"user_id": "123456789",
|
|
"timestamp": "2025-10-21T19:46:00Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
## Game Action Events
|
|
|
|
### 1. Set Defensive Positioning
|
|
|
|
**Client → Server**
|
|
```json
|
|
{
|
|
"event": "set_defense",
|
|
"data": {
|
|
"game_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"positioning": "standard" // "standard", "infield_in", "shift_left", "shift_right"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Server → Client** (Acknowledgment)
|
|
```json
|
|
{
|
|
"event": "decision_recorded",
|
|
"data": {
|
|
"type": "set_defense",
|
|
"positioning": "standard",
|
|
"timestamp": "2025-10-21T19:45:25Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Server → All Participants** (Next Decision Required)
|
|
```json
|
|
{
|
|
"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**
|
|
```json
|
|
{
|
|
"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**
|
|
```json
|
|
{
|
|
"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)
|
|
```json
|
|
{
|
|
"event": "dice_rolled",
|
|
"data": {
|
|
"roll": 14,
|
|
"animation_duration": 2000,
|
|
"timestamp": "2025-10-21T19:45:30Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Server → All Participants** (Play Outcome)
|
|
```json
|
|
{
|
|
"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**
|
|
```json
|
|
{
|
|
"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**
|
|
```json
|
|
{
|
|
"event": "select_play_result",
|
|
"data": {
|
|
"game_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"play_number": 45,
|
|
"selection": "single_center"
|
|
}
|
|
}
|
|
```
|
|
|
|
### 6. Substitution
|
|
|
|
**Client → Server**
|
|
```json
|
|
{
|
|
"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**
|
|
```json
|
|
{
|
|
"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**
|
|
```json
|
|
{
|
|
"event": "change_pitcher",
|
|
"data": {
|
|
"game_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"pitcher_out": 11111,
|
|
"pitcher_in": 22222
|
|
}
|
|
}
|
|
```
|
|
|
|
**Server → All Participants**
|
|
```json
|
|
{
|
|
"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**
|
|
```json
|
|
{
|
|
"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**
|
|
```json
|
|
{
|
|
"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**
|
|
```json
|
|
{
|
|
"event": "decision_timeout_warning",
|
|
"data": {
|
|
"decision_type": "set_defense",
|
|
"seconds_remaining": 10,
|
|
"default_action": "standard"
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4. Auto-Decision Made
|
|
|
|
**Server → All Participants**
|
|
```json
|
|
{
|
|
"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**
|
|
```json
|
|
{
|
|
"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**
|
|
```json
|
|
{
|
|
"event": "connection_error",
|
|
"data": {
|
|
"code": "AUTH_FAILED",
|
|
"message": "Invalid or expired token",
|
|
"reconnect": false
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Game Error
|
|
|
|
**Server → All Participants**
|
|
```json
|
|
{
|
|
"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
|
|
```json
|
|
{
|
|
"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
|
|
1. Client detects disconnect
|
|
2. Client attempts reconnect with backoff (1s, 2s, 4s, 8s, 16s)
|
|
3. On successful reconnect, client sends `join_game` event
|
|
4. Server checks if game state exists in memory
|
|
5. If not, server recovers state from database
|
|
6. Server sends full `game_state_update` to client
|
|
7. Client resumes from current state
|
|
|
|
### Client Implementation
|
|
```typescript
|
|
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
|
|
```python
|
|
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
|
|
```javascript
|
|
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](./backend-architecture.md) for connection manager implementation. |