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