strat-gameplay-webapp/.claude/implementation/websocket-protocol.md
Cal Corum 5c75b935f0 CLAUDE: Initial project setup - documentation and infrastructure
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)
2025-10-21 16:21:13 -05:00

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

  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

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.