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>
336 lines
10 KiB
Markdown
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)
|