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