strat-gameplay-webapp/.claude/DECISION_REQUIRED_IMPLEMENTATION.md
Cal Corum f3eb5e8200 CLAUDE: Add WebSocket protocol specification and implementation guides
Comprehensive documentation for real-time gameplay workflow:

**New Documentation**:

1. WEBSOCKET_PROTOCOL_SPEC.md (49KB)
   - Complete catalog of all 15 backend WebSocket event handlers
   - Complete catalog of all frontend event listeners
   - Game workflow sequences (connection → game start → play resolution)
   - Critical issues identified and resolution status
   - Event payload specifications with examples
   - Timing and performance expectations

2. DECISION_REQUIRED_IMPLEMENTATION.md (11KB)
   - Issue #1 detailed analysis and resolution
   - Backend implementation of decision_required event
   - Frontend integration approach
   - Before/After workflow comparison
   - Test validation results

3. GAMEPLAY_SESSION_HANDOFF.md (10KB)
   - Session work summary and accomplishments
   - Live testing results and observations
   - Known issues and next steps
   - Quick start guide for next session
   - Technical architecture notes

**Why**:
- Provides single source of truth for WebSocket protocol
- Documents complete event flow for frontend/backend alignment
- Captures implementation decisions and rationale
- Enables faster onboarding for new developers
- Creates reference for debugging WebSocket issues

**Impact**:
- Reduces protocol confusion between frontend and backend
- Accelerates future WebSocket feature development
- Provides clear integration patterns for new events

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 15:41:16 -06:00

336 lines
10 KiB
Markdown

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