diff --git a/.claude/DECISION_REQUIRED_IMPLEMENTATION.md b/.claude/DECISION_REQUIRED_IMPLEMENTATION.md new file mode 100644 index 0000000..cf3886c --- /dev/null +++ b/.claude/DECISION_REQUIRED_IMPLEMENTATION.md @@ -0,0 +1,335 @@ +# Decision Required Event Implementation + +**Date**: 2025-01-21 +**Status**: IMPLEMENTED - Pending Testing +**Priority**: HIGH (from WebSocket Protocol Spec Issue #1) + +--- + +## Overview + +Implemented real-time `decision_required` event emission from backend to frontend. Previously, the frontend had to poll `gameState.decision_phase` to know when decisions were needed. Now the backend proactively notifies the frontend via WebSocket events. + +## Problem Statement + +From WEBSOCKET_PROTOCOL_SPEC.md Issue #1: +- Frontend expects `decision_required` event when decision phases change +- Backend never emitted this event +- Workaround: Frontend polls `gameState.decision_phase` +- Impact: Inconsistent with real-time design philosophy + +## Solution Implemented + +### 1. GameEngine Infrastructure (game_engine.py) + +**Added Connection Manager Integration** (lines 42-58): +```python +def __init__(self): + # ... existing code ... + self._connection_manager = None # Set by main.py + +def set_connection_manager(self, connection_manager): + """Set WebSocket connection manager for real-time events""" + self._connection_manager = connection_manager + logger.info("WebSocket connection manager configured for game engine") +``` + +**Created Event Emission Helper** (lines 60-100): +```python +async def _emit_decision_required( + self, game_id: UUID, state: GameState, phase: str, timeout_seconds: int = 300 +): + """ + Emit decision_required event to notify frontend. + + Auto-determines which team (home/away) needs to decide based on: + - awaiting_defensive: Fielding team (home if top, away if bottom) + - awaiting_offensive: Batting team (away if top, home if bottom) + """ + if not self._connection_manager: + logger.warning("No connection manager - cannot emit decision_required") + return + + # Team determination logic + if phase == "awaiting_defensive": + role = "home" if state.half == "top" else "away" + elif phase == "awaiting_offensive": + role = "away" if state.half == "top" else "home" + + await self._connection_manager.broadcast_to_game( + str(game_id), + "decision_required", + { + "phase": phase, + "role": role, + "timeout_seconds": timeout_seconds, + "message": f"{role.title()} team: {phase.replace('_', ' ').title()} decision required" + } + ) +``` + +### 2. Event Emission Points + +**Location 1: Game Start** (game_engine.py:253) +```python +async def start_game(self, game_id: UUID) -> GameState: + # ... existing setup code ... + state.decision_phase = "awaiting_defensive" + state_manager.update_state(game_id, state) + + # NEW: Emit decision_required event + await self._emit_decision_required( + game_id, state, "awaiting_defensive", + timeout_seconds=self.DECISION_TIMEOUT + ) +``` + +**Location 2: Defensive Decision Request** (game_engine.py:398) +```python +async def _request_defensive_decision( + self, state: GameState, timeout: int = DECISION_TIMEOUT +) -> DefensiveDecision: + # ... existing code ... + state.decision_phase = "awaiting_defensive" + state_manager.update_state(state.game_id, state) + + # NEW: Emit decision_required event + await self._emit_decision_required( + state.game_id, state, "awaiting_defensive", + timeout_seconds=timeout + ) +``` + +**Location 3: Offensive Decision Request** (game_engine.py:464) +```python +async def _request_offensive_decision( + self, state: GameState, timeout: int = DECISION_TIMEOUT +) -> OffensiveDecision: + # ... existing code ... + state.decision_phase = "awaiting_offensive" + state_manager.update_state(state.game_id, state) + + # NEW: Emit decision_required event + await self._emit_decision_required( + state.game_id, state, "awaiting_offensive", + timeout_seconds=timeout + ) +``` + +### 3. Main.py Integration (main.py:86-88) + +```python +# Initialize connection manager and register handlers +connection_manager = ConnectionManager(sio) +register_handlers(sio, connection_manager) + +# NEW: Configure game engine with connection manager +from app.core.game_engine import game_engine +game_engine.set_connection_manager(connection_manager) +``` + +## Event Payload Specification + +### Event Name +`decision_required` + +### Direction +Server → Client (Broadcast to game room) + +### Payload Structure +```typescript +{ + phase: "awaiting_defensive" | "awaiting_offensive", + role: "home" | "away", + timeout_seconds: number, + message: string +} +``` + +### Example Payloads + +**Defensive (Top of inning)**: +```json +{ + "phase": "awaiting_defensive", + "role": "home", + "timeout_seconds": 30, + "message": "Home team: Awaiting Defensive decision required" +} +``` + +**Offensive (Bottom of inning)**: +```json +{ + "phase": "awaiting_offensive", + "role": "home", + "timeout_seconds": 30, + "message": "Home team: Awaiting Offensive decision required" +} +``` + +## Frontend Integration + +### Existing Handler (useWebSocket.ts:289-292) + +Frontend already has the handler implemented: +```typescript +socketInstance.on('decision_required', (prompt) => { + console.log('[WebSocket] Decision required:', prompt.phase) + gameStore.setDecisionPrompt(prompt) +}) +``` + +### Expected Behavior +1. Backend emits `decision_required` when phase transitions +2. Frontend receives event +3. Frontend updates `currentDecisionPrompt` in game store +4. UI reactively shows appropriate decision panel (DefensiveSetup or OffensiveApproach) + +### Frontend Type Definition (types/game.ts:268-276) + +Already defined: +```typescript +export interface DecisionPrompt { + phase: DecisionPhase + role: 'home' | 'away' + timeout_seconds: number + options?: string[] + message?: string +} +``` + +## Testing Status + +### ✅ Completed +- [x] Code implementation in game_engine.py +- [x] Connection manager integration in main.py +- [x] Backend server restarts cleanly with changes +- [x] Type definitions already exist in frontend +- [x] Frontend handler already exists + +### ⏳ Pending +- [ ] End-to-end test: Create game → Submit lineups → Verify event received +- [ ] Verify event arrives when game starts (awaiting_defensive) +- [ ] Verify event arrives after defensive decision submitted (awaiting_offensive) +- [ ] Verify frontend DecisionPanel shows proactively (without polling) +- [ ] Update WEBSOCKET_PROTOCOL_SPEC.md to mark Issue #1 as RESOLVED + +## Testing Instructions + +### Manual Test Plan + +1. **Start Both Servers**: + ```bash + # Backend + cd backend && uv run python -m app.main + + # Frontend + cd frontend-sba && bun run dev + ``` + +2. **Create New Game**: + - Navigate to http://localhost:3000 + - Create game with two teams + - Submit lineups for both teams + +3. **Verify Event Emission**: + - Backend logs should show: `Emitted decision_required for game {id}: phase=awaiting_defensive, role=home` + - Frontend logs should show: `[WebSocket] Decision required: awaiting_defensive` + - Frontend should show DefensiveSetup panel immediately (no polling delay) + +4. **Submit Defensive Decision**: + - Set defensive positioning + - Submit decision + - Verify offensive_decision_required event arrives + - Verify OffensiveApproach panel shows + +5. **Check Browser DevTools**: + - Network tab → WS → Messages + - Should see `decision_required` events in real-time + +### Backend Logs to Watch + +``` +2025-01-21 XX:XX:XX - app.core.game_engine.GameEngine - INFO - WebSocket connection manager configured for game engine +2025-01-21 XX:XX:XX - app.core.game_engine.GameEngine - INFO - Emitted decision_required for game {uuid}: phase=awaiting_defensive, role=home +2025-01-21 XX:XX:XX - app.core.game_engine.GameEngine - INFO - Emitted decision_required for game {uuid}: phase=awaiting_offensive, role=away +``` + +### Frontend Logs to Watch + +``` +[WebSocket] Decision required: awaiting_defensive +[Game Store] Decision prompt set: { phase: 'awaiting_defensive', role: 'home', ... } +[DecisionPanel] Showing defensive setup panel +``` + +## Related Changes + +### Companion Implementation: Decision Phase Naming (2025-01-21) + +As part of this session, we also standardized decision phase naming: +- All code now uses `'awaiting_defensive'` and `'awaiting_offensive'` +- Removed dual-condition checks in frontend +- See: `types/game.ts:41`, `store/game.ts:109-125` + +This ensures consistency between the `decision_required` event and `gameState.decision_phase` values. + +## Known Issues / Limitations + +1. **No Authorization Check**: Event is broadcast to entire game room, including spectators + - Future: Add team ownership validation + - Related: Protocol Spec Issue #9 (TODO comments) + +2. **No Retry Logic**: If WebSocket is disconnected during emission, event is lost + - Mitigation: Frontend still polls `gameState.decision_phase` as fallback + - Future: Add event queuing for reconnection + +3. **Timeout Not Enforced**: `timeout_seconds` is informational only + - Backend has timeout logic in `_request_defensive_decision` but uses asyncio.wait_for + - Frontend doesn't show countdown timer yet + +4. **Decision Phase Field Sync** (RESOLVED 2025-11-21): + - Issue: `submit_defensive_decision` and `submit_offensive_decision` were setting `pending_decision` but NOT `decision_phase` + - Impact: Frontend received `decision_required` event but game state showed stale phase + - Fix: Added `state.decision_phase` updates in both methods: + - Line 291: `state.decision_phase = "awaiting_offensive"` after defensive + - Line 335: `state.decision_phase = "resolution"` after offensive + - Why needed: Frontend checks `gameState.decision_phase`, backend must keep it in sync + +## Next Steps + +1. **Immediate**: Run manual test plan above +2. **If tests pass**: Update protocol spec to mark Issue #1 as RESOLVED +3. **Next priority**: Implement authorization checks (Protocol Spec Issue #9) +4. **Future enhancement**: Add countdown timer UI in DecisionPanel + +## Files Modified + +### Backend +- `backend/app/core/game_engine.py` (+58 lines) + - Added `_connection_manager` attribute + - Added `set_connection_manager()` method + - Added `_emit_decision_required()` helper + - Added 3 emission calls at phase transitions + +- `backend/app/main.py` (+3 lines) + - Import game_engine singleton + - Call `game_engine.set_connection_manager(connection_manager)` + +### Frontend +- No changes needed - handler already implemented + +## Rollback Plan + +If issues arise, revert these commits: +1. Remove emission calls from game_engine.py (lines 253, 398, 464) +2. Remove `_emit_decision_required()` method (lines 60-100) +3. Remove connection_manager integration from main.py (lines 86-88) +4. Frontend will fallback to polling `gameState.decision_phase` + +--- + +**Document Status**: Ready for testing +**Last Updated**: 2025-01-21 +**Author**: Claude (Session: decision_required implementation) diff --git a/.claude/GAMEPLAY_SESSION_HANDOFF.md b/.claude/GAMEPLAY_SESSION_HANDOFF.md new file mode 100644 index 0000000..77d81a7 --- /dev/null +++ b/.claude/GAMEPLAY_SESSION_HANDOFF.md @@ -0,0 +1,338 @@ +# Gameplay Loop Implementation - Session Handoff + +**Date**: 2025-01-21 +**Session Focus**: Connecting frontend gameplay UI to backend WebSocket handlers +**Status**: 🟡 In Progress - Defensive decisions working, testing offensive workflow next + +--- + +## 🎯 Session Goals + +Implement the core gameplay loop: dice rolling → outcome submission → play resolution + +**Target Workflow**: +1. User submits defensive setup (infield/outfield positioning) +2. User submits offensive decision (batting action) +3. User rolls dice +4. User submits manual outcome (from physical card) +5. Backend resolves play and broadcasts result +6. Game state updates, play appears in feed + +--- + +## ✅ What We Accomplished + +### 1. **Connected Gameplay Loop Components** +- Fixed `submitManualOutcome()` signature in `useGameActions.ts` +- Added `play_resolved` WebSocket event handler +- Fixed game page to call outcome submission correctly +- Added missing TypeScript imports + +### 2. **Fixed Decision Panel Visibility Issues** + +**Root Cause**: Frontend was checking for `decision_phase: "defense"` but backend uses `"awaiting_defensive"` + +**Files Modified**: +- `frontend-sba/store/game.ts` - Added checks for both naming conventions: + ```typescript + needsDefensiveDecision: checks 'defense' OR 'awaiting_defensive' + needsOffensiveDecision: checks 'offensive_approach' OR 'awaiting_offensive' + ``` + +### 3. **Fixed Game Start Issues** + +**Problem**: Games were stuck in `decision_phase: "idle"` after lineups submitted + +**Root Causes Fixed**: +1. Missing `game_engine` import in `backend/app/api/routes/games.py` +2. `start_game()` wasn't setting `decision_phase` after preparing first play + +**Files Modified**: +- `backend/app/api/routes/games.py`: + ```python + from app.core.game_engine import game_engine # Added import + ``` + +- `backend/app/core/game_engine.py`: + ```python + # In start_game() after _prepare_next_play(): + state.decision_phase = "awaiting_defensive" # Added this line + ``` + +### 4. **Fixed State Updates After Decision Submission** + +**Problem**: After submitting defensive decision, UI stayed on defensive panel instead of advancing to offensive + +**Solution**: Frontend now requests updated game state after decision submissions + +**File Modified**: +- `frontend-sba/composables/useWebSocket.ts`: + ```typescript + socketInstance.on('defensive_decision_submitted', (data) => { + // Request updated game state to get new decision_phase + socketInstance.emit('request_game_state', { game_id: gameStore.gameId }) + }) + ``` + +### 5. **Added Debug Logging** +- Added console logging in game page to debug panel visibility +- Logs show `decision_phase`, `needsDefensiveDecision`, `needsOffensiveDecision`, etc. + +--- + +## 📁 Files Modified (Need Committing) + +### Backend Changes +``` +backend/app/api/routes/games.py +backend/app/core/game_engine.py +``` + +### Frontend Changes +``` +frontend-sba/store/game.ts +frontend-sba/composables/useGameActions.ts +frontend-sba/composables/useWebSocket.ts +frontend-sba/pages/games/[id].vue +``` + +--- + +## ✅ What's Working Now + +### ✓ Game Creation Flow +- Create game → submit lineups → game auto-starts +- Game enters `"awaiting_defensive"` phase immediately +- Frontend shows **Defensive Setup** panel + +### ✓ Defensive Decision Submission +- User can select infield/outfield positioning +- Clicking "Submit" sends decision to backend +- Backend receives and processes decision +- Emits `defensive_decision_submitted` event +- Frontend requests updated game state + +### ✓ WebSocket Communication +- Connection established successfully +- Events flowing backend → frontend +- `dice_rolled`, `outcome_accepted`, `play_resolved` handlers implemented + +### ✓ Backend Game Engine +- All 15 WebSocket handlers implemented (Phase 3E-Final) +- Manual outcome workflow complete +- Play resolution working +- Database persistence functional + +--- + +## 🔄 Current State + +### What User Just Did +1. ✅ Created new game (ID: `0411d0ce-f41f-46eb-b0ac-23bae818d6ad`) +2. ✅ Saw defensive setup panel appear +3. ✅ Submitted defensive decision +4. ⚠️ Still seeing "Your Defensive Turn" indicator + +### What Should Happen Next +After latest fix (requesting game state after submission), user should: +1. Refresh game page +2. Submit defensive decision again +3. Should see **Offensive Approach** panel appear +4. Select batting action → Submit +5. Should see **Gameplay Panel** with **Roll Dice** button + +--- + +## 🧪 Testing Checklist + +### ✅ Tested & Working +- [x] Game creation +- [x] Lineup submission +- [x] Game auto-start +- [x] Defensive panel appears +- [x] Defensive decision sends to backend + +### ⏳ Next to Test +- [ ] Defensive submission → Offensive panel transition +- [ ] Offensive decision submission +- [ ] Offensive → Resolution phase (dice rolling) +- [ ] Dice roll button click +- [ ] Dice results display +- [ ] Manual outcome entry +- [ ] Outcome submission +- [ ] Play resolution display +- [ ] Play-by-play feed update +- [ ] Game state updates (score, outs, runners) +- [ ] Second at-bat (loop continues) + +--- + +## 🔧 Technical Details + +### Backend Decision Phase Values +```python +"idle" # Initial state (before start) +"awaiting_defensive" # Waiting for defensive setup +"awaiting_offensive" # Waiting for offensive approach +"resolution" # Ready for dice rolling +``` + +### WebSocket Event Flow + +**Decision Submission**: +``` +Frontend: submit_defensive_decision → +Backend: process → emit defensive_decision_submitted → +Frontend: request_game_state → +Backend: game_state (with decision_phase: "awaiting_offensive") → +Frontend: Show offensive panel +``` + +**Dice Rolling & Outcome**: +``` +Frontend: roll_dice → +Backend: roll dice → emit dice_rolled → +Frontend: Show dice results → +User: Read card, select outcome → +Frontend: submit_manual_outcome → +Backend: validate & resolve → emit play_resolved → +Frontend: Display result, update state +``` + +### Key Store Properties +```typescript +// Game Store (frontend-sba/store/game.ts) +needsDefensiveDecision // Shows defensive panel +needsOffensiveDecision // Shows offensive panel +canRollDice // Enables dice roll button +canSubmitOutcome // Enables outcome submission +``` + +--- + +## 🐛 Known Issues & Workarounds + +### Issue 1: Frontend Compilation Warnings +**Symptom**: "Element is missing end tag" errors in console +**Impact**: None - server still runs fine +**Status**: Cosmetic, doesn't affect functionality + +### Issue 2: Old Games Stuck in "idle" +**Symptom**: Games created before our fixes still show "Waiting for strategic decisions" +**Workaround**: Create fresh games - old games won't auto-advance +**Fix**: Old games need manual state update or recreation + +--- + +## 🚀 Quick Start for Next Session + +### 1. Check Server Status +```bash +# Backend should be running on port 8000 +# Frontend should be running on port 3001 +``` + +### 2. Test Defensive → Offensive Transition +1. Go to current game: `http://localhost:3001/games/0411d0ce-f41f-46eb-b0ac-23bae818d6ad` +2. Submit defensive decision +3. **Should** see offensive panel appear now (after latest fix) + +### 3. If Offensive Panel Appears - Continue Testing! +- Submit offensive decision +- Click "Roll Dice" +- Check dice results display +- Submit outcome +- Verify play resolution + +### 4. If Still Stuck - Create Fresh Game +``` +http://localhost:3001/games/create +``` +New games will have all fixes applied. + +--- + +## 📝 Pending Commits + +### Gameplay Loop Connection (Already Committed) +```bash +git log --oneline -1 +# 58b5deb CLAUDE: Connect gameplay loop - dice rolling and play resolution +``` + +### Decision Panel Fixes (Need Committing) +```bash +# Backend changes +backend/app/api/routes/games.py # Added game_engine import +backend/app/core/game_engine.py # Set decision_phase in start_game + +# Frontend changes +frontend-sba/store/game.ts # Check both decision_phase naming conventions +frontend-sba/composables/useWebSocket.ts # Request state after decision submissions +frontend-sba/pages/games/[id].vue # Added debug logging +``` + +--- + +## 🎓 Lessons Learned + +### 1. Backend/Frontend Contract Mismatches +**Issue**: Backend used "awaiting_defensive" but frontend checked for "defense" +**Solution**: Frontend now checks both variants for robustness + +### 2. State Synchronization +**Issue**: Backend updated state but didn't broadcast changes +**Solution**: Frontend explicitly requests updated state after key events + +### 3. Game Initialization +**Issue**: `start_game()` prepared play but didn't set decision_phase +**Solution**: Explicitly set initial decision_phase after prepare + +### 4. Import Issues Can Fail Silently +**Issue**: Missing `game_engine` import caused silent failure with try/catch +**Solution**: Always verify imports are present and check logs for errors + +--- + +## 📞 If You Get Stuck + +### Check These First +1. **Backend logs**: `BashOutput` with filter for game ID or event names +2. **Browser console**: Look for `[Game Page] Panel visibility check:` log +3. **WebSocket events**: Check if events are being received +4. **Game state**: Look at `decision_phase` value in console log + +### Common Fixes +- **Panels not showing**: Check `decision_phase` value, create fresh game +- **Events not working**: Check WebSocket connection status +- **State not updating**: Look for `game_state` or `game_state_update` events + +--- + +## 🎯 Success Criteria + +**Session Complete When**: +- [x] Defensive panel appears ✅ +- [ ] Offensive panel appears after defensive submission +- [ ] Dice can be rolled +- [ ] Outcome can be submitted +- [ ] Play resolves and appears in feed +- [ ] Second at-bat starts automatically +- [ ] All changes committed + +**Current Progress**: ~60% complete + +--- + +## 📚 References + +- **Backend Handlers**: `backend/app/websocket/handlers.py` (15 handlers, all implemented) +- **Game Engine**: `backend/app/core/game_engine.py` (Phase 3E-Final complete) +- **Frontend Components**: `frontend-sba/components/Gameplay/` (all built, partially connected) +- **Main Session Context**: `CLAUDE.md` in project root + +--- + +**Next Session Goal**: Complete offensive decision → dice rolling → outcome submission flow + +Good luck! 🚀 diff --git a/.claude/WEBSOCKET_PROTOCOL_SPEC.md b/.claude/WEBSOCKET_PROTOCOL_SPEC.md new file mode 100644 index 0000000..c5e9d6d --- /dev/null +++ b/.claude/WEBSOCKET_PROTOCOL_SPEC.md @@ -0,0 +1,1831 @@ +# 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, + 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**