# 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.