# WebSocket Protocol Specification
## Strat-O-Matic Baseball Game Engine

**Version**: 1.1
**Date**: 2025-11-21
**Status**: Analysis Complete + Issue #1 Resolved (decision_required implemented)

---

## Executive Summary

This document defines the complete WebSocket communication protocol between the frontend (Vue 3/Nuxt) and backend (FastAPI/Socket.io) for real-time baseball gameplay. It identifies the complete game workflow, all 15 backend event handlers, all frontend event listeners, and critical gaps/mismatches discovered during analysis.

## Table of Contents

1. [Game Workflow Overview](#game-workflow-overview)
2. [WebSocket Events - Complete Reference](#websocket-events---complete-reference)
3. [Game State Flow](#game-state-flow)
4. [Critical Issues Identified](#critical-issues-identified)
5. [Event Sequences by Workflow](#event-sequences-by-workflow)
6. [Type Mappings](#type-mappings)
7. [Recommendations](#recommendations)

---

## Game Workflow Overview

### High-Level Flow

```
1. Game Creation (REST API)
   ├─→ POST /games (create game)
   ├─→ POST /games/{id}/lineups (submit lineups)
   └─→ Game auto-starts after lineups submitted

2. WebSocket Connection
   ├─→ Client connects with JWT token
   ├─→ Backend validates and accepts connection
   ├─→ Client joins game room
   └─→ Client requests game state

3. Decision Phase Workflow (Per Play)
   ├─→ Backend emits decision_required (defense)
   ├─→ Defense submits decision
   ├─→ Backend emits decision_required (offense)
   ├─→ Offense submits decision
   └─→ Backend advances to resolution phase

4. Resolution Workflow
   ├─→ Manual Mode:
   │   ├─→ Client rolls dice
   │   ├─→ Backend broadcasts dice results
   │   ├─→ Client submits outcome from card
   │   └─→ Backend resolves play and broadcasts result
   │
   └─→ Auto Mode (future):
       └─→ Backend auto-resolves and broadcasts

5. Play Completion
   ├─→ Backend broadcasts play_resolved
   ├─→ Backend broadcasts game_state_update
   └─→ Loop back to step 3 (next play)

6. Game End
   └─→ Backend broadcasts game_ended
```

---

## WebSocket Events - Complete Reference

### Connection Events

#### `connect` (Internal Socket.io)
**Direction**: Client → Server
**Purpose**: Establish WebSocket connection with JWT authentication
**Backend**: `handlers.py:26-50`
**Frontend**: N/A (Socket.io internal)

**Auth Data**:
```typescript
{
  token: string  // JWT from Discord OAuth
}
```

**Backend Behavior**:
- Validates JWT token
- Extracts user_id from token
- Registers connection in ConnectionManager
- Returns `true` to accept, `false` to reject

**Success**: Emits `connected` event
**Failure**: Rejects connection

---

#### `connected` ✅
**Direction**: Server → Client
**Purpose**: Confirm connection successful
**Backend**: `handlers.py:43`
**Frontend**: `useWebSocket.ts:234`

**Payload**:
```typescript
{
  user_id: string
}
```

**Frontend Handling**:
- Logs user_id
- No state changes (already set `isConnected=true` from Socket.io `connect`)

---

#### `disconnect` (Internal Socket.io)
**Direction**: Client → Server
**Purpose**: Clean up when client disconnects
**Backend**: `handlers.py:53-55`
**Frontend**: `useWebSocket.ts:201`

**Backend Behavior**:
- Removes from all game rooms
- Cleans up user_sessions map
- No broadcast to other clients

**Frontend Behavior**:
- Sets `isConnected = false`
- Shows warning toast
- Attempts auto-reconnection
- Broadcasts to game store: `gameStore.setConnected(false)`

---

#### `heartbeat` / `heartbeat_ack` ✅
**Direction**: Bidirectional
**Purpose**: Keep connection alive
**Backend**: `handlers.py:91-93`
**Frontend**: `useWebSocket.ts:238, 455-463`

**Flow**:
1. Client sends `heartbeat` every 30 seconds
2. Server responds with `heartbeat_ack`
3. Client receives ack (connection healthy)

**No payload required**

---

### Game Room Management

#### `join_game` ✅
**Direction**: Client → Server
**Purpose**: Join game room as player or spectator
**Backend**: `handlers.py:58-77`
**Frontend**: `useGameActions.ts:69-78`

**Request**:
```typescript
{
  game_id: string,
  role: 'player' | 'spectator'
}
```

**Backend Behavior**:
- Validates game_id
- TODO: Verify user has access to game
- Adds client to game room
- Emits `game_joined` confirmation

**Success Response**: `game_joined` event

---

#### `game_joined` ✅
**Direction**: Server → Client
**Purpose**: Confirm successful join
**Backend**: `handlers.py:71-73`
**Frontend**: Not explicitly handled (should be added)

**Payload**:
```typescript
{
  game_id: string,
  role: 'player' | 'spectator'
}
```

⚠️ **ISSUE**: Frontend doesn't have explicit handler for this event

---

#### `leave_game` ✅
**Direction**: Client → Server
**Purpose**: Leave game room
**Backend**: `handlers.py:80-88`
**Frontend**: `useGameActions.ts:83-94`

**Request**:
```typescript
{
  game_id: string
}
```

**Backend Behavior**:
- Removes client from game room
- No broadcast (silent exit)

**Frontend Behavior**:
- Resets game store
- Called on component unmount

---

### Game State Synchronization

#### `request_game_state` ✅
**Direction**: Client → Server
**Purpose**: Request full game state (initial load or reconnection)
**Backend**: `handlers.py:96-138`
**Frontend**: `useGameActions.ts:283-293`

**Request**:
```typescript
{
  game_id: string
}
```

**Backend Behavior**:
- Validates game_id UUID format
- Checks in-memory state first
- If not in memory, recovers from database
- Serializes with `model_dump(mode='json')` for UUIDs

**Success Response**: `game_state` event
**Failure Response**: `error` event with "Game not found"

---

#### `game_state` ✅
**Direction**: Server → Client
**Purpose**: Send complete game state (response to request_game_state)
**Backend**: `handlers.py:127`
**Frontend**: `useWebSocket.ts:247-250`

**Payload**: Full `GameState` object
```typescript
{
  game_id: string,
  league_id: 'sba' | 'pd',
  home_team_id: number,
  away_team_id: number,
  status: 'pending' | 'active' | 'paused' | 'completed',
  inning: number,
  half: 'top' | 'bottom',
  outs: number,
  balls: number,
  strikes: number,
  home_score: number,
  away_score: number,
  on_first: LineupPlayerState | null,
  on_second: LineupPlayerState | null,
  on_third: LineupPlayerState | null,
  current_batter: LineupPlayerState,
  current_pitcher: LineupPlayerState | null,
  current_catcher: LineupPlayerState | null,
  decision_phase: 'defense' | 'stolen_base' | 'offensive_approach' | 'resolution' | 'complete',
  pending_decision: string | null,
  decisions_this_play: Record<string, boolean>,
  pending_defensive_decision: DefensiveDecision | null,
  pending_offensive_decision: OffensiveDecision | null,
  pending_manual_roll: RollData | null,
  play_count: number,
  // ... additional fields
}
```

**Frontend Handling**:
- Calls `gameStore.setGameState(state)`
- Completely replaces existing game state
- Does NOT update play history (use `game_state_sync` for that)

---

#### `game_state_update` ✅
**Direction**: Server → Client
**Purpose**: Broadcast incremental state change to all players
**Backend**: Not directly emitted (only via game_engine)
**Frontend**: `useWebSocket.ts:252-255`

**Payload**: Full `GameState` object (same as `game_state`)

**Frontend Handling**:
- Calls `gameStore.setGameState(state)`
- Replaces state entirely (not incremental)

**Usage**: Backend broadcasts this after:
- Decision submissions
- Play resolutions
- Inning changes
- Score changes

⚠️ **NAMING CONFUSION**: Both `game_state` and `game_state_update` send full state, not partial updates

---

#### `game_state_sync` ✅
**Direction**: Server → Client
**Purpose**: Full state + recent play history (for reconnection)
**Backend**: Not implemented yet
**Frontend**: `useWebSocket.ts:257-264`

**Payload**:
```typescript
{
  state: GameState,
  recent_plays: PlayResult[],
  timestamp: string
}
```

**Frontend Handling**:
- Sets game state
- Adds recent plays to history
- Used for reconnection recovery

⚠️ **IMPLEMENTATION STATUS**: Frontend has handler, backend doesn't emit yet

---

### Strategic Decisions

#### `submit_defensive_decision` ✅
**Direction**: Client → Server
**Purpose**: Submit defensive team's strategic setup
**Backend**: `handlers.py:1048-1137`
**Frontend**: `useGameActions.ts:103-114`

**Request**:
```typescript
{
  game_id: string,
  infield_depth: 'infield_in' | 'normal' | 'corners_in',
  outfield_depth: 'normal' | 'shallow',
  hold_runners: number[]  // Bases to hold (e.g., [1, 3])
}
```

**Backend Behavior**:
- Validates game exists
- TODO: Verify user is fielding team manager
- Calls `game_engine.submit_defensive_decision()`
- Updates state.decision_phase
- Broadcasts result

**Success Response**: `defensive_decision_submitted` event

---

#### `defensive_decision_submitted` ✅
**Direction**: Server → Client (Broadcast)
**Purpose**: Notify all players defense is set
**Backend**: `handlers.py:1116-1129`
**Frontend**: `useWebSocket.ts:294-310`

**Payload**:
```typescript
{
  game_id: string,
  decision: {
    infield_depth: string,
    outfield_depth: string,
    hold_runners: number[]
  },
  pending_decision: 'offensive' | 'resolution' | null
}
```

**Frontend Handling**:
- Clears decision prompt
- Requests updated game state
- Shows toast based on pending_decision
- If still pending → "Waiting for offense..."
- If no pending → "Ready to play!"

---

#### `submit_offensive_decision` ✅
**Direction**: Client → Server
**Purpose**: Submit offensive team's strategy
**Backend**: `handlers.py:1140-1217`
**Frontend**: `useGameActions.ts:119-129`

**Request**:
```typescript
{
  game_id: string,
  action: 'swing_away' | 'steal' | 'check_jump' | 'hit_and_run' | 'sac_bunt' | 'squeeze_bunt',
  steal_attempts: number[]  // Required when action="steal"
}
```

**Backend Behavior**:
- Validates game exists
- TODO: Verify user is batting team manager
- Creates OffensiveDecision model
- Calls `game_engine.submit_offensive_decision()`
- Broadcasts result

**Success Response**: `offensive_decision_submitted` event

---

#### `offensive_decision_submitted` ✅
**Direction**: Server → Client (Broadcast)
**Purpose**: Notify all players offense is set
**Backend**: `handlers.py:1200-1209`
**Frontend**: `useWebSocket.ts:312-324`

**Payload**:
```typescript
{
  game_id: string,
  decision: {
    action: string,
    steal_attempts: number[]
  },
  pending_decision: 'resolution' | null
}
```

**Frontend Handling**:
- Clears decision prompt
- Requests updated game state
- Shows "Ready to play!" toast

---

#### `decision_required` ✅ IMPLEMENTED
**Direction**: Server → Client
**Purpose**: Prompt player for decision
**Backend**: `game_engine.py:60-100, 308-310` (Implemented 2025-11-21)
**Frontend**: `useWebSocket.ts:289-292`

**Payload**:
```typescript
{
  phase: 'awaiting_defensive' | 'awaiting_offensive' | 'awaiting_stolen_base' | 'resolution',
  role: 'home' | 'away',
  timeout_seconds: number,
  message: string
}
```

**Backend Implementation**:
- Emitted at game start (awaiting_defensive)
- Emitted after defensive decision submitted (awaiting_offensive)
- Auto-determines team role based on inning half
- Uses `_emit_decision_required()` helper method

**Frontend Handling**:
- Calls `gameStore.setDecisionPrompt(prompt)`
- DecisionPanel component shows UI based on phase
- Updates proactively without polling

✅ **RESOLVED**: Backend now emits this event at appropriate phase transitions

---

### Manual Outcome Workflow

#### `roll_dice` ✅
**Direction**: Client → Server
**Purpose**: Roll dice for manual play resolution
**Backend**: `handlers.py:141-219`
**Frontend**: `useGameActions.ts:138-153`

**Request**:
```typescript
{
  game_id: string
}
```

**Backend Behavior**:
- Validates game exists
- TODO: Verify user is participant
- Calls `dice_system.roll_ab()` for cryptographic random rolls
- Stores roll in `state.pending_manual_roll`
- Broadcasts dice results to all players

**Success Response**: `dice_rolled` event (broadcast)

---

#### `dice_rolled` ✅
**Direction**: Server → Client (Broadcast)
**Purpose**: Show dice results to all players
**Backend**: `handlers.py:198-213`
**Frontend**: `useWebSocket.ts:330-345`

**Payload**:
```typescript
{
  game_id: string,
  roll_id: string,
  d6_one: number,           // First d6
  d6_two_total: number,     // Sum of second 2d6
  chaos_d20: number,        // Chaos die
  resolution_d20: number,   // Resolution die
  check_wild_pitch: boolean,
  check_passed_ball: boolean,
  timestamp: string,
  message: string
}
```

**Frontend Handling**:
- Stores in `gameStore.setPendingRoll()`
- Shows toast with message
- Enables outcome submission UI
- NOTE: Frontend adds d6_two_a and d6_two_b (not provided by backend)

⚠️ **MINOR ISSUE**: Frontend expects individual d6_two dice, backend only sends total

---

#### `submit_manual_outcome` ✅
**Direction**: Client → Server
**Purpose**: Submit card reading result
**Backend**: `handlers.py:222-437`
**Frontend**: `useGameActions.ts:158-175`

**Request**:
```typescript
{
  game_id: string,
  outcome: string,  // PlayOutcome enum value
  hit_location?: string  // Required for groundballs/flyballs
}
```

**Backend Behavior**:
- Validates game exists
- Validates using ManualOutcomeSubmission model
- Checks for pending_manual_roll
- Validates hit_location if required
- Confirms acceptance
- Clears pending_manual_roll (one-time use)
- Calls `game_engine.resolve_manual_play()`
- Broadcasts play result

**Success Responses**:
1. `outcome_accepted` (to submitter)
2. `play_resolved` (broadcast to all)

**Failure Response**: `outcome_rejected`

---

#### `outcome_accepted` ✅
**Direction**: Server → Client (To submitter only)
**Purpose**: Confirm outcome was valid
**Backend**: `handlers.py:341-349`
**Frontend**: `useWebSocket.ts:347-351`

**Payload**:
```typescript
{
  game_id: string,
  outcome: string,
  hit_location?: string
}
```

**Frontend Handling**:
- Clears pending roll
- Shows "Submitting..." toast

---

#### `play_resolved` ✅
**Direction**: Server → Client (Broadcast)
**Purpose**: Broadcast complete play result
**Backend**: `handlers.py:409-411`
**Frontend**: `useWebSocket.ts:353-376`

**Payload**:
```typescript
{
  game_id: string,
  play_number: number,
  outcome: string,           // Resolved outcome (may differ from submitted)
  hit_location?: string,
  description: string,        // Human-readable play description
  outs_recorded: number,
  runs_scored: number,
  batter_result: number | null,  // Where batter ended up (1-4, null=out)
  runners_advanced: RunnerAdvancement[],
  is_hit: boolean,
  is_out: boolean,
  is_walk: boolean,
  roll_id: string,
  x_check_details?: XCheckResult  // If defensive play required X-Check
}
```

**Frontend Handling**:
- Converts to PlayResult type
- Adds to play history: `gameStore.addPlayToHistory()`
- Stores last result: `gameStore.setLastPlayResult()`
- Shows description toast

---

#### `outcome_rejected` ✅
**Direction**: Server → Client (To submitter only)
**Purpose**: Notify submission was invalid
**Backend**: `handlers.py:297-298, 420-421`
**Frontend**: `useWebSocket.ts:424-427`

**Payload**:
```typescript
{
  message: string,
  field: string,
  errors?: Array<{
    loc: string[],
    msg: string,
    type: string
  }>
}
```

**Frontend Handling**:
- Shows error toast with message

**Common Rejection Reasons**:
- Missing hit_location for groundball/flyball
- No pending dice roll
- Invalid outcome enum value
- Invalid hit_location format

---

### Play Progression

#### `play_completed` ✅
**Direction**: Server → Client (Broadcast)
**Purpose**: Legacy event (may be redundant with play_resolved)
**Backend**: Not explicitly emitted in handlers
**Frontend**: `useWebSocket.ts:266-270`

**Payload**: `PlayResult` object

**Frontend Handling**:
- Adds to play history
- Shows description toast

⚠️ **REDUNDANCY**: Both `play_completed` and `play_resolved` exist. Recommend consolidating.

---

#### `inning_change` ✅
**Direction**: Server → Client (Broadcast)
**Purpose**: Notify inning transition
**Backend**: Not explicitly emitted in handlers
**Frontend**: `useWebSocket.ts:272-275`

**Payload**:
```typescript
{
  inning: number,
  half: 'top' | 'bottom'
}
```

**Frontend Handling**:
- Shows toast: "Top 1" or "Bottom 9", etc.

---

#### `game_ended` ✅
**Direction**: Server → Client (Broadcast)
**Purpose**: Notify game completion
**Backend**: Not explicitly emitted in handlers
**Frontend**: `useWebSocket.ts:277-283`

**Payload**:
```typescript
{
  game_id: string,
  winner_team_id: number,
  final_score: {
    home: number,
    away: number
  },
  completed_at: string
}
```

**Frontend Handling**:
- Shows game over toast with final score

---

### Substitutions

#### `request_pinch_hitter` ✅
**Direction**: Client → Server
**Purpose**: Replace current batter
**Backend**: `handlers.py:442-593`
**Frontend**: `useGameActions.ts:184-201`

**Request**:
```typescript
{
  game_id: string,
  player_out_lineup_id: number,
  player_in_card_id: number,
  team_id: number
}
```

**Backend Behavior**:
- Validates all required fields
- TODO: Verify user is team manager
- Creates SubstitutionManager instance
- Calls `sub_manager.pinch_hit()`
- Broadcasts result

**Success Responses**:
1. `player_substituted` (broadcast)
2. `substitution_confirmed` (to requester)

**Failure Response**: `substitution_error`

---

#### `request_defensive_replacement` ✅
**Direction**: Client → Server
**Purpose**: Replace defensive player
**Backend**: `handlers.py:596-761`
**Frontend**: `useGameActions.ts:206-225`

**Request**:
```typescript
{
  game_id: string,
  player_out_lineup_id: number,
  player_in_card_id: number,
  new_position: string,  // e.g., "SS"
  team_id: number
}
```

**Backend Behavior**:
- Validates all required fields
- Creates SubstitutionManager instance
- Calls `sub_manager.defensive_replace()`
- Broadcasts result

**Success Responses**:
1. `player_substituted` (broadcast)
2. `substitution_confirmed` (to requester)

**Failure Response**: `substitution_error`

---

#### `request_pitching_change` ✅
**Direction**: Client → Server
**Purpose**: Replace current pitcher
**Backend**: `handlers.py:764-917`
**Frontend**: `useGameActions.ts:230-247`

**Request**:
```typescript
{
  game_id: string,
  player_out_lineup_id: number,
  player_in_card_id: number,
  team_id: number
}
```

**Backend Behavior**:
- Validates all required fields
- Creates SubstitutionManager instance
- Calls `sub_manager.change_pitcher()`
- Validates pitcher has faced 1+ batter (unless injury)
- Broadcasts result

**Success Responses**:
1. `player_substituted` (broadcast)
2. `substitution_confirmed` (to requester)

**Failure Response**: `substitution_error`

---

#### `player_substituted` ✅
**Direction**: Server → Client (Broadcast)
**Purpose**: Notify all players of substitution
**Backend**: `handlers.py:544-557, 710-723, 866-879`
**Frontend**: `useWebSocket.ts:382-392`

**Payload**:
```typescript
{
  type: 'pinch_hitter' | 'defensive_replacement' | 'pitching_change',
  player_out_lineup_id: number,
  player_in_card_id: number,
  new_lineup_id: number,
  position: string,
  batting_order: number | null,
  team_id: number,
  message: string
}
```

**Frontend Handling**:
- Shows toast with message
- Requests updated lineup for affected team

---

#### `substitution_confirmed` ✅
**Direction**: Server → Client (To requester only)
**Purpose**: Confirm substitution success
**Backend**: `handlers.py:560-568, 726-734, 882-890`
**Frontend**: `useWebSocket.ts:394-397`

**Payload**:
```typescript
{
  type: string,
  new_lineup_id: number,
  success: true,
  message: string
}
```

**Frontend Handling**:
- Shows success toast

---

#### `substitution_error` ✅
**Direction**: Server → Client (To requester only)
**Purpose**: Notify substitution failed
**Backend**: `handlers.py:576-587, 742-753, 898-909`
**Frontend**: `useWebSocket.ts:429-432`

**Payload**:
```typescript
{
  message: string,
  code: string,
  type: string
}
```

**Frontend Handling**:
- Shows error toast

**Common Error Codes**:
- `MISSING_FIELD`: Required parameter missing
- `INVALID_FORMAT`: game_id not valid UUID
- `PLAYER_NOT_FOUND`: lineup_id doesn't exist
- `PLAYER_NOT_ACTIVE`: Player already substituted
- `NO_ELIGIBLE_REPLACEMENT`: Bench player not eligible

---

### Data Requests

#### `get_lineup` ✅
**Direction**: Client → Server
**Purpose**: Get full lineup with player data
**Backend**: `handlers.py:920-1045`
**Frontend**: `useGameActions.ts:256-265`

**Request**:
```typescript
{
  game_id: string,
  team_id: number
}
```

**Backend Behavior**:
- Checks state_manager cache first (O(1) lookup)
- If not cached, loads from database with player data
- Caches for future requests
- Returns full lineup including nested player info

**Success Response**: `lineup_data` event

---

#### `lineup_data` ✅
**Direction**: Server → Client (To requester only)
**Purpose**: Return lineup with player details
**Backend**: `handlers.py:964-989, 1006-1030`
**Frontend**: `useWebSocket.ts:403-406`

**Payload**:
```typescript
{
  game_id: string,
  team_id: number,
  players: Array<{
    lineup_id: number,
    card_id: number,
    position: string,
    batting_order: number | null,
    is_active: boolean,
    is_starter: boolean,
    player: {
      id: number,
      name: string,
      image: string,
      headshot: string
    }
  }>
}
```

**Frontend Handling**:
- Calls `gameStore.updateLineup(team_id, players)`
- Caches for player name/headshot lookups

**Critical Optimization**: Lineup sent once per team, not on every state update

---

#### `get_box_score` ✅
**Direction**: Client → Server
**Purpose**: Get game statistics
**Backend**: `handlers.py:1220-1276`
**Frontend**: `useGameActions.ts:270-278`

**Request**:
```typescript
{
  game_id: string
}
```

**Backend Behavior**:
- Queries materialized views for stats
- Returns batting and pitching stats for both teams

**Success Response**: `box_score_data` event

---

#### `box_score_data` ✅
**Direction**: Server → Client (To requester only)
**Purpose**: Return game statistics
**Backend**: `handlers.py:1257-1260`
**Frontend**: `useWebSocket.ts:408-412`

**Payload**:
```typescript
{
  game_id: string,
  box_score: {
    game_stats: {
      home_runs: number,
      away_runs: number,
      home_hits: number,
      away_hits: number,
      home_errors: number,
      away_errors: number,
      linescore_home: number[],
      linescore_away: number[]
    },
    batting_stats: BattingStatLine[],
    pitching_stats: PitchingStatLine[]
  }
}
```

**Frontend Handling**:
- Logs for now
- TODO: Display in BoxScore component

---

### Error Events

#### `error` ✅
**Direction**: Server → Client
**Purpose**: Generic error message
**Backend**: Used throughout handlers
**Frontend**: `useWebSocket.ts:418-422`

**Payload**:
```typescript
{
  message: string,
  code?: string,
  field?: string,
  hint?: string
}
```

**Frontend Handling**:
- Sets error in game store
- Shows error toast (7 seconds)

**Usage**: Validation failures, game not found, unauthorized actions

---

#### `invalid_action` ❓
**Direction**: Server → Client
**Purpose**: Action not allowed in current state
**Backend**: Not emitted in handlers
**Frontend**: `useWebSocket.ts:434-437`

**Payload**:
```typescript
{
  action: string,
  reason: string
}
```

**Frontend Handling**:
- Shows error toast

⚠️ **IMPLEMENTATION STATUS**: Frontend has handler, backend never emits

---

#### `connection_error` ❓
**Direction**: Server → Client
**Purpose**: Connection-level error
**Backend**: Not emitted in handlers
**Frontend**: `useWebSocket.ts:439-443`

**Payload**:
```typescript
{
  error: string,
  details?: string
}
```

**Frontend Handling**:
- Stores error
- Shows error toast

⚠️ **IMPLEMENTATION STATUS**: Frontend has handler, backend never emits

---

## Game State Flow

### Complete Play Lifecycle

```
START: Game active, awaiting decisions

1. Backend checks decision_phase
   └─→ 'defense' or 'awaiting_defensive'

2. Frontend checks gameState.decision_phase
   └─→ Shows DefensiveSetup UI if fielding team

3. Defense submits decision
   Client: submit_defensive_decision → Server

4. Backend processes
   └─→ Calls game_engine.submit_defensive_decision()
   └─→ Updates state.decision_phase = 'offensive_approach'
   └─→ Broadcasts defensive_decision_submitted

5. Frontend receives defensive_decision_submitted
   └─→ Clears decision prompt
   └─→ Requests updated game_state
   └─→ Checks pending_decision field

6. Frontend checks updated gameState.decision_phase
   └─→ Now 'offensive_approach' or 'awaiting_offensive'
   └─→ Shows OffensiveApproach UI if batting team

7. Offense submits decision
   Client: submit_offensive_decision → Server

8. Backend processes
   └─→ Calls game_engine.submit_offensive_decision()
   └─→ Updates state.decision_phase = 'resolution'
   └─→ Broadcasts offensive_decision_submitted

9. Frontend receives offensive_decision_submitted
   └─→ Clears decision prompt
   └─→ Requests updated game_state
   └─→ Decision phase now 'resolution'

10. Frontend shows GameplayPanel
    └─→ Checks canRollDice = (decision_phase === 'resolution' && !pendingRoll)
    └─→ Enables "Roll Dice" button

11. User clicks "Roll Dice"
    Client: roll_dice → Server

12. Backend rolls dice
    └─→ Calls dice_system.roll_ab()
    └─→ Stores in state.pending_manual_roll
    └─→ Broadcasts dice_rolled to all players

13. Frontend receives dice_rolled
    └─→ Stores in gameStore.pendingRoll
    └─→ Shows dice animation
    └─→ Enables outcome submission UI

14. User reads card and selects outcome
    Client: submit_manual_outcome → Server

15. Backend validates and accepts
    └─→ Sends outcome_accepted to submitter
    └─→ Clears state.pending_manual_roll
    └─→ Calls game_engine.resolve_manual_play()

16. Game engine resolves play
    └─→ Processes outcome with X-Check if needed
    └─→ Updates runners, score, outs
    └─→ Advances batter
    └─→ Checks for inning change
    └─→ Persists play to database

17. Backend broadcasts results
    └─→ Emits play_resolved to all players
    └─→ Emits game_state_update with new state

18. Frontend receives play_resolved
    └─→ Adds to play history
    └─→ Shows play description
    └─→ Clears pending roll

19. Frontend receives game_state_update
    └─→ Updates game state
    └─→ Checks decision_phase

20. Loop back to step 1 for next play
```

### State Transition Diagram

```
                    ┌─────────────────────────┐
                    │   Game Created          │
                    │   (status: pending)     │
                    └────────────┬────────────┘
                                 │
                                 │ Lineups submitted
                                 ↓
                    ┌─────────────────────────┐
                    │   Game Active           │
                    │   (status: active)      │
                    │   inning: 1, half: top  │
                    └────────────┬────────────┘
                                 │
                 ┌───────────────┴───────────────┐
                 │                               │
                 ↓                               ↓
    ┌─────────────────────┐        ┌─────────────────────┐
    │  Defense Phase      │        │  Stored in DB       │
    │  (decision_phase:   │        │  (persisted state)  │
    │   'defense')        │        └─────────────────────┘
    └──────────┬──────────┘
               │ submit_defensive_decision
               ↓
    ┌─────────────────────┐
    │  Offense Phase      │
    │  (decision_phase:   │
    │   'offensive_       │
    │    approach')       │
    └──────────┬──────────┘
               │ submit_offensive_decision
               ↓
    ┌─────────────────────┐
    │  Resolution Phase   │
    │  (decision_phase:   │
    │   'resolution')     │
    └──────────┬──────────┘
               │
               ├─ roll_dice → dice_rolled
               │
               ├─ submit_manual_outcome → outcome_accepted
               │
               └─ play_resolved → game_state_update
                         │
                         ↓
        ┌────────────────────────────┐
        │  Check Inning Complete?    │
        └────────┬───────────────────┘
                 │
       ┌─────────┴─────────┐
       │ No                │ Yes
       ↓                   ↓
 Loop to next     ┌─────────────────┐
 at-bat           │  Inning Change  │
 (Defense Phase)  │  inning_change  │
                  └────────┬────────┘
                           │
                           ├─ Advance to next half/inning
                           │
                           └─ Check Game Complete?
                                      │
                            ┌─────────┴─────────┐
                            │ No                │ Yes
                            ↓                   ↓
                      Loop to next    ┌─────────────────┐
                      inning          │   Game Ended    │
                      (Defense Phase) │   game_ended    │
                                      │   (status:      │
                                      │    completed)   │
                                      └─────────────────┘
```

---

## Critical Issues Identified

### 1. Missing Backend Event Emission ✅ RESOLVED (2025-11-21)

**Issue**: Frontend expects `decision_required` event, backend never emits it.

**Resolution**: Implemented `decision_required` event emission in three locations:

1. **Game Start** (`game_engine.py:253`): Emits `awaiting_defensive` when game starts
2. **After Defensive Decision** (`game_engine.py:308-310`): Emits `awaiting_offensive` after defense submits
3. **Event Helper** (`game_engine.py:60-100`): `_emit_decision_required()` method with team determination logic

**Implementation Details**:
- Added `_connection_manager` attribute to `GameEngine` singleton
- Added `set_connection_manager()` called from `main.py:88`
- Auto-determines team role based on inning half and decision phase
- Broadcasts to entire game room via WebSocket

**Critical Fix Applied During Testing**:
- Also updated `state.decision_phase` field in both `submit_defensive_decision` (line 291) and `submit_offensive_decision` (line 335)
- Frontend checks `gameState.decision_phase`, so state must be in sync with events
- See `.claude/DECISION_REQUIRED_IMPLEMENTATION.md` Issue #4 for details

**Testing Results**:
- ✅ Event emitted at game start (awaiting_defensive)
- ✅ Event emitted after defensive decision (awaiting_offensive)
- ✅ Frontend DecisionPanel updates proactively without polling
- ✅ Game state `decision_phase` field stays in sync

**Files Modified**:
- `backend/app/core/game_engine.py` (+65 lines)
- `backend/app/main.py` (+3 lines)
- Frontend already had handler implemented

---

### 2. Event Naming Inconsistency ⚠️ MEDIUM PRIORITY

**Issue**: Three different events send full game state:
- `game_state` (response to request)
- `game_state_update` (broadcast after changes)
- `game_state_sync` (reconnection with plays)

**Problem**: All three send the FULL GameState object, but naming suggests `game_state_update` is incremental.

**Recommendation**:
- Rename `game_state_update` → `game_state_broadcast`
- Reserve `game_state_update` for future incremental updates
- Or consolidate: only use `game_state` for all cases

---

### 3. Missing `d6_two_a` and `d6_two_b` in Dice Roll ℹ️ LOW PRIORITY

**Backend** (`handlers.py:204`):
```python
"d6_two_total": ab_roll.d6_two_total,
```

**Frontend** (`useWebSocket.ts:335-336`):
```typescript
d6_two_a: 0, // Not provided by server
d6_two_b: 0, // Not provided by server
```

**Impact**: Frontend can't show individual 2d6 dice animation

**Recommendation**: Add to backend emission:
```python
"d6_two_a": ab_roll.d6_two_a,
"d6_two_b": ab_roll.d6_two_b,
```

---

### 4. Redundant Events ℹ️ LOW PRIORITY

**Issue**: Both `play_completed` and `play_resolved` exist for same purpose.

**Frontend Handling**:
- `play_completed`: Adds to history, shows toast
- `play_resolved`: Adds to history, shows toast (identical behavior)

**Recommendation**: Remove `play_completed`, use only `play_resolved`

---

### 5. Missing Error Event Implementations ℹ️ LOW PRIORITY

**Frontend Handlers with No Backend Emission**:
- `invalid_action` (defined in types, handler exists)
- `connection_error` (defined in types, handler exists)

**Recommendation**:
- Either implement backend emission
- Or remove unused frontend handlers

---

### 6. `game_joined` Not Handled ⚠️ MEDIUM PRIORITY

**Backend** (`handlers.py:71-73`):
```python
await manager.emit_to_user(
    sid, "game_joined", {"game_id": game_id, "role": role}
)
```

**Frontend**: No handler for this event

**Impact**: Client doesn't confirm successful room join

**Recommendation**: Add handler in `useWebSocket.ts`:
```typescript
socketInstance.on('game_joined', (data) => {
  console.log('[WebSocket] Successfully joined game:', data.game_id, 'as', data.role)
  gameStore.setGameJoined(true)
  uiStore.showSuccess(`Joined game as ${data.role}`)
})
```

---

### 7. Decision Phase Naming Mismatch ✅ RESOLVED (2025-01-21)

**Issue**: Frontend and backend used inconsistent names for decision phases.

**Resolution**: Standardized on backend convention using `'awaiting_*'` prefix:
- `'awaiting_defensive'` - Defense must submit
- `'awaiting_offensive'` - Offense must submit
- `'awaiting_stolen_base'` - Stolen base decision
- `'resolution'` - Ready for dice/outcome
- `'complete'` - Play resolved

**Changes Made**:
- ✅ Updated `frontend-sba/types/game.ts` DecisionPhase type
- ✅ Simplified `frontend-sba/store/game.ts` conditionals (removed dual checks)
- ✅ Updated test files with new phase names
- ✅ Verified backend already uses standard naming
- ✅ Type checking passes

**Impact**: Cleaner code, no more dual-condition checks, consistent naming across stack

---

### 8. Missing Auto Mode Implementation ℹ️ INFO

**Current State**: Only manual mode implemented (roll dice + submit outcome)

**Future**: Auto mode should:
1. Skip `roll_dice` step
2. Backend automatically resolves outcome
3. Emits `play_resolved` directly

**No Code Changes Needed**: This is expected future work, not a bug.

---

### 9. TODO Comments Need Resolution ⚠️ MEDIUM PRIORITY

**Across Backend Handlers**:
```python
# TODO: Verify user is participant in this game
# TODO: Verify user is authorized to submit for this team
# TODO: Verify user has access to view lineup
```

**Impact**: No authorization checks currently enforced

**Security Risk**: Any authenticated user can:
- Submit decisions for any team
- Make substitutions for any team
- View any game's data

**Recommendation**: Implement authorization layer before production:
```python
def is_team_manager(user_id: str, team_id: int) -> bool:
    # Check user owns this team
    pass

def is_game_participant(user_id: str, game_id: UUID) -> bool:
    # Check user is player or spectator in game
    pass
```

---

## Event Sequences by Workflow

### Game Creation and Start

```
1. REST: POST /games
   Response: { game_id, status: "pending" }

2. REST: POST /games/{id}/lineups
   Backend: Auto-starts game
   Response: { message: "Lineups submitted" }

3. WebSocket: connect (with JWT)
   Server → Client: connected { user_id }

4. WebSocket: join_game { game_id, role: "player" }
   Server → Client: game_joined { game_id, role }

5. WebSocket: request_game_state { game_id }
   Server → Client: game_state { ...full state }

6. WebSocket: get_lineup { game_id, team_id: home }
   Server → Client: lineup_data { players: [...] }

7. WebSocket: get_lineup { game_id, team_id: away }
   Server → Client: lineup_data { players: [...] }

READY TO PLAY
```

---

### Decision Submission Workflow

```
SCENARIO: Top of 1st, no outs, bases empty

1. Frontend checks gameState.decision_phase
   Value: 'defense' or 'awaiting_defensive'

2. Fielding team (home) sees DefensiveSetup UI
   User selects: infield_depth='normal', outfield_depth='normal'

3. Client: submit_defensive_decision
   Request: { game_id, infield_depth: 'normal', outfield_depth: 'normal', hold_runners: [] }

4. Backend: game_engine.submit_defensive_decision()
   - Stores decision in state.pending_defensive_decision
   - Transitions state.decision_phase = 'offensive_approach'
   - Persists to database

5. Server → All Clients: defensive_decision_submitted
   Payload: { decision: {...}, pending_decision: 'offensive' }

6. Frontend: Clears decision UI, requests game_state
   Server → Client: game_state { decision_phase: 'offensive_approach' }

7. Batting team (away) sees OffensiveApproach UI
   User selects: action='swing_away', steal_attempts=[]

8. Client: submit_offensive_decision
   Request: { game_id, action: 'swing_away', steal_attempts: [] }

9. Backend: game_engine.submit_offensive_decision()
   - Stores decision in state.pending_offensive_decision
   - Transitions state.decision_phase = 'resolution'
   - Persists to database

10. Server → All Clients: offensive_decision_submitted
    Payload: { decision: {...}, pending_decision: 'resolution' }

11. Frontend: Shows GameplayPanel with "Roll Dice" button
```

---

### Manual Outcome Workflow

```
CONTINUING FROM ABOVE: decision_phase = 'resolution'

1. User clicks "Roll Dice"
   Client: roll_dice { game_id }

2. Backend: dice_system.roll_ab()
   Result: { d6_one: 4, d6_two_total: 7, chaos_d20: 12, resolution_d20: 8 }
   Stores in state.pending_manual_roll

3. Server → All Clients: dice_rolled (broadcast)
   Payload: { roll_id, d6_one: 4, d6_two_total: 7, chaos: 12, resolution: 8, ... }

4. Frontend: Stores pending roll, shows dice animation
   Enables outcome selection UI

5. User reads batter card (4-7 column, d20=12)
   Card shows: "SINGLE_1" (single, batter to 1st)

6. User clicks "Submit" with outcome
   Client: submit_manual_outcome { game_id, outcome: 'SINGLE_1' }

7. Backend: Validates outcome, clears pending roll
   Server → Client: outcome_accepted (to submitter)

8. Backend: game_engine.resolve_manual_play()
   - Processes SINGLE_1
   - Moves batter to 1st base
   - Increments play count
   - Advances batting order
   - Checks decision_phase → transitions back to 'defense'
   - Persists play to database

9. Server → All Clients: play_resolved (broadcast)
   Payload: {
     play_number: 1,
     outcome: 'SINGLE_1',
     description: 'Mike Trout singles to right field',
     outs_recorded: 0,
     runs_scored: 0,
     batter_result: 1,
     runners_advanced: [{ from: 0, to: 1 }],
     ...
   }

10. Server → All Clients: game_state_update (broadcast)
    Payload: { ...full updated game state }

11. Frontend: Updates game state
    - Adds play to history
    - Shows play description toast
    - Clears pending roll
    - Updates scoreboard (on_first now populated)

12. Frontend checks decision_phase
    Value: 'defense' (back to step 1 for next batter)
```

---

### Substitution Workflow

```
SCENARIO: Bottom of 3rd, manager wants pinch hitter

1. User clicks floating "Substitutions" button
   Shows SubstitutionPanel modal

2. User selects:
   - Substitute type: Pinch Hitter
   - Player out: Lineup ID 42 (batting order 5)
   - Player in: Card ID 1234 (from bench)

3. Client: request_pinch_hitter
   Request: {
     game_id,
     player_out_lineup_id: 42,
     player_in_card_id: 1234,
     team_id: 101
   }

4. Backend: SubstitutionManager.pinch_hit()
   - Validates player_out is active
   - Validates player_in exists and not used
   - Marks player_out as inactive
   - Creates new lineup entry for player_in
   - Assigns batting order from player_out
   - Persists to database

5. Server → All Clients: player_substituted (broadcast)
   Payload: {
     type: 'pinch_hitter',
     player_out_lineup_id: 42,
     player_in_card_id: 1234,
     new_lineup_id: 98,
     position: 'RF',
     batting_order: 5,
     team_id: 101,
     message: 'Pinch hitter: #5 now batting'
   }

6. Server → Requester: substitution_confirmed
   Payload: { type: 'pinch_hitter', new_lineup_id: 98, success: true }

7. Frontend: Shows success toast, requests updated lineup
   Client: get_lineup { game_id, team_id: 101 }

8. Server → Client: lineup_data
   Payload: { players: [...updated lineup with new player] }

9. Frontend: Updates cached lineup
   gameStore.updateLineup(101, players)

10. User closes substitution modal
```

---

## Type Mappings

### Backend Models → Frontend Types

| Backend (Python) | Frontend (TypeScript) | File |
|-----------------|----------------------|------|
| `GameState` | `GameState` | types/game.ts:61 |
| `LineupPlayerState` | `LineupPlayerState` | types/game.ts:44 |
| `DefensiveDecision` | `DefensiveDecision` | types/game.ts:124 |
| `OffensiveDecision` | `OffensiveDecision` | types/game.ts:136 |
| `PlayResult` | `PlayResult` | types/game.ts:217 |
| `AbRoll` | `RollData` | types/game.ts:145 |
| `PlayOutcome` (enum) | `PlayOutcome` (union) | types/game.ts:161 |
| `Lineup` (db model) | `Lineup` | types/player.ts |

### WebSocket Event Types

Complete type definitions:

**Client → Server**:
```typescript
interface ClientToServerEvents {
  // Connection
  join_game: (data: JoinGameRequest) => void
  leave_game: (data: LeaveGameRequest) => void
  heartbeat: () => void

  // Decisions
  submit_defensive_decision: (data: DefensiveDecisionRequest) => void
  submit_offensive_decision: (data: OffensiveDecisionRequest) => void

  // Manual workflow
  roll_dice: (data: RollDiceRequest) => void
  submit_manual_outcome: (data: SubmitManualOutcomeRequest) => void

  // Substitutions
  request_pinch_hitter: (data: PinchHitterRequest) => void
  request_defensive_replacement: (data: DefensiveReplacementRequest) => void
  request_pitching_change: (data: PitchingChangeRequest) => void

  // Data
  get_lineup: (data: GetLineupRequest) => void
  get_box_score: (data: GetBoxScoreRequest) => void
  request_game_state: (data: RequestGameStateRequest) => void
}
```

**Server → Client**:
```typescript
interface ServerToClientEvents {
  // Connection
  connected: (data: ConnectedEvent) => void
  game_joined: (data: GameJoinedEvent) => void
  heartbeat_ack: () => void

  // Decisions
  decision_required: (data: DecisionPrompt) => void
  defensive_decision_submitted: (data: DefensiveDecisionSubmittedEvent) => void
  offensive_decision_submitted: (data: OffensiveDecisionSubmittedEvent) => void

  // State
  game_state: (data: GameState) => void
  game_state_update: (data: GameState) => void
  game_state_sync: (data: GameStateSyncEvent) => void

  // Plays
  play_completed: (data: PlayResult) => void
  play_resolved: (data: PlayResult) => void
  inning_change: (data: InningChangeEvent) => void
  game_ended: (data: GameEndedEvent) => void

  // Manual workflow
  dice_rolled: (data: DiceRolledEvent) => void
  outcome_accepted: (data: OutcomeAcceptedEvent) => void

  // Substitutions
  player_substituted: (data: SubstitutionResult) => void
  substitution_confirmed: (data: SubstitutionConfirmedEvent) => void

  // Data
  lineup_data: (data: LineupDataResponse) => void
  box_score_data: (data: BoxScoreDataEvent) => void

  // Errors
  error: (data: ErrorEvent) => void
  outcome_rejected: (data: OutcomeRejectedEvent) => void
  substitution_error: (data: SubstitutionError) => void
  invalid_action: (data: InvalidActionEvent) => void
  connection_error: (data: ConnectionErrorEvent) => void
}
```

---

## Recommendations

### Immediate Action Items (Pre-Launch)

1. ~~**Implement `decision_required` emission**~~ ✅ COMPLETED (2025-11-21)
   - Added to game_engine after phase transitions
   - Real-time decision prompting now working
   - Also fixed `decision_phase` field synchronization

2. **Add `game_joined` frontend handler** ⚠️ MEDIUM
   - Confirm room join success
   - Better error handling if join fails

3. ~~**Standardize decision phase naming**~~ ✅ COMPLETED (2025-01-21)
   - Standardized on `'awaiting_*'` convention
   - Reduces conditional complexity

4. **Add authorization checks** ⚠️ HIGH SECURITY
   - Verify team ownership before actions
   - Prevent unauthorized substitutions/decisions

5. **Include d6_two individual dice** ℹ️ LOW
   - Better dice animation UX
   - Trivial backend change

### Future Improvements

6. **Consolidate state broadcast events** ℹ️ MEDIUM
   - Unify `game_state`, `game_state_update`, `game_state_sync`
   - Clearer semantics

7. **Remove redundant `play_completed`** ℹ️ LOW
   - Use only `play_resolved`
   - Simplifies protocol

8. **Implement auto mode** ℹ️ FUTURE
   - Skip manual dice/outcome workflow
   - Faster gameplay option

9. **Add incremental state updates** ⚠️ OPTIMIZATION
   - Instead of full GameState (~2KB), send diffs (~200 bytes)
   - Reduces bandwidth 10x
   - Important for mobile 3G connections

10. **Add connection recovery logic** ⚠️ MEDIUM
    - Detect missed events during brief disconnect
    - Request only missing plays, not full state

---

## Appendix A: Backend Handlers Summary

**File**: `backend/app/websocket/handlers.py`

| Handler | Lines | Description |
|---------|-------|-------------|
| `connect` | 26-50 | JWT auth and accept |
| `disconnect` | 53-55 | Cleanup |
| `join_game` | 58-77 | Join room |
| `leave_game` | 80-88 | Leave room |
| `heartbeat` | 91-93 | Keep-alive |
| `request_game_state` | 96-138 | Get full state |
| `roll_dice` | 141-219 | Manual dice roll |
| `submit_manual_outcome` | 222-437 | Manual outcome |
| `request_pinch_hitter` | 442-593 | Pinch hitter sub |
| `request_defensive_replacement` | 596-761 | Defensive sub |
| `request_pitching_change` | 764-917 | Pitcher sub |
| `get_lineup` | 920-1045 | Get lineup data |
| `submit_defensive_decision` | 1048-1137 | Defense strategy |
| `submit_offensive_decision` | 1140-1217 | Offense strategy |
| `get_box_score` | 1220-1276 | Game statistics |

**Total**: 15 handlers, 1,251 lines

---

## Appendix B: Frontend Event Listeners Summary

**File**: `frontend-sba/composables/useWebSocket.ts`

| Event | Lines | Description |
|-------|-------|-------------|
| `connect` (Socket.io) | 187-199 | Connection established |
| `disconnect` (Socket.io) | 201-216 | Connection lost |
| `connect_error` (Socket.io) | 218-232 | Connection failed |
| `connected` | 234-236 | Server confirms |
| `heartbeat_ack` | 238-241 | Keep-alive ack |
| `game_state` | 247-250 | Full state |
| `game_state_update` | 252-255 | State broadcast |
| `game_state_sync` | 257-264 | State + plays |
| `play_completed` | 266-270 | Play finished |
| `inning_change` | 272-275 | Inning transition |
| `game_ended` | 277-283 | Game over |
| `decision_required` | 289-292 | Need input |
| `defensive_decision_submitted` | 294-310 | Defense set |
| `offensive_decision_submitted` | 312-324 | Offense set |
| `dice_rolled` | 330-345 | Dice results |
| `outcome_accepted` | 347-351 | Outcome valid |
| `play_resolved` | 353-376 | Play complete |
| `player_substituted` | 382-392 | Sub broadcast |
| `substitution_confirmed` | 394-397 | Sub success |
| `lineup_data` | 403-406 | Lineup info |
| `box_score_data` | 408-412 | Stats data |
| `error` | 418-422 | Generic error |
| `outcome_rejected` | 424-427 | Invalid outcome |
| `substitution_error` | 429-432 | Sub failed |
| `invalid_action` | 434-437 | Action denied |
| `connection_error` | 439-443 | Connection issue |

**Total**: 26 listeners, 257 lines

---

## Document Change Log

| Date | Version | Changes |
|------|---------|---------|
| 2025-01-21 | 1.0 | Initial comprehensive specification |
| 2025-11-21 | 1.1 | Issue #1 resolved: Implemented `decision_required` event emission in backend, updated event descriptions, marked recommendations as complete |

---

**End of WebSocket Protocol Specification**
