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