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>
10 KiB
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_requiredevent 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):
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):
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)
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)
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)
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)
# 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
{
phase: "awaiting_defensive" | "awaiting_offensive",
role: "home" | "away",
timeout_seconds: number,
message: string
}
Example Payloads
Defensive (Top of inning):
{
"phase": "awaiting_defensive",
"role": "home",
"timeout_seconds": 30,
"message": "Home team: Awaiting Defensive decision required"
}
Offensive (Bottom of inning):
{
"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:
socketInstance.on('decision_required', (prompt) => {
console.log('[WebSocket] Decision required:', prompt.phase)
gameStore.setDecisionPrompt(prompt)
})
Expected Behavior
- Backend emits
decision_requiredwhen phase transitions - Frontend receives event
- Frontend updates
currentDecisionPromptin game store - UI reactively shows appropriate decision panel (DefensiveSetup or OffensiveApproach)
Frontend Type Definition (types/game.ts:268-276)
Already defined:
export interface DecisionPrompt {
phase: DecisionPhase
role: 'home' | 'away'
timeout_seconds: number
options?: string[]
message?: string
}
Testing Status
✅ Completed
- Code implementation in game_engine.py
- Connection manager integration in main.py
- Backend server restarts cleanly with changes
- Type definitions already exist in frontend
- 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
-
Start Both Servers:
# Backend cd backend && uv run python -m app.main # Frontend cd frontend-sba && bun run dev -
Create New Game:
- Navigate to http://localhost:3000
- Create game with two teams
- Submit lineups for both teams
-
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)
- Backend logs should show:
-
Submit Defensive Decision:
- Set defensive positioning
- Submit decision
- Verify offensive_decision_required event arrives
- Verify OffensiveApproach panel shows
-
Check Browser DevTools:
- Network tab → WS → Messages
- Should see
decision_requiredevents 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
-
No Authorization Check: Event is broadcast to entire game room, including spectators
- Future: Add team ownership validation
- Related: Protocol Spec Issue #9 (TODO comments)
-
No Retry Logic: If WebSocket is disconnected during emission, event is lost
- Mitigation: Frontend still polls
gameState.decision_phaseas fallback - Future: Add event queuing for reconnection
- Mitigation: Frontend still polls
-
Timeout Not Enforced:
timeout_secondsis informational only- Backend has timeout logic in
_request_defensive_decisionbut uses asyncio.wait_for - Frontend doesn't show countdown timer yet
- Backend has timeout logic in
-
Decision Phase Field Sync (RESOLVED 2025-11-21):
- Issue:
submit_defensive_decisionandsubmit_offensive_decisionwere settingpending_decisionbut NOTdecision_phase - Impact: Frontend received
decision_requiredevent but game state showed stale phase - Fix: Added
state.decision_phaseupdates in both methods:- Line 291:
state.decision_phase = "awaiting_offensive"after defensive - Line 335:
state.decision_phase = "resolution"after offensive
- Line 291:
- Why needed: Frontend checks
gameState.decision_phase, backend must keep it in sync
- Issue:
Next Steps
- Immediate: Run manual test plan above
- If tests pass: Update protocol spec to mark Issue #1 as RESOLVED
- Next priority: Implement authorization checks (Protocol Spec Issue #9)
- Future enhancement: Add countdown timer UI in DecisionPanel
Files Modified
Backend
-
backend/app/core/game_engine.py(+58 lines)- Added
_connection_managerattribute - Added
set_connection_manager()method - Added
_emit_decision_required()helper - Added 3 emission calls at phase transitions
- Added
-
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:
- Remove emission calls from game_engine.py (lines 253, 398, 464)
- Remove
_emit_decision_required()method (lines 60-100) - Remove connection_manager integration from main.py (lines 86-88)
- Frontend will fallback to polling
gameState.decision_phase
Document Status: Ready for testing Last Updated: 2025-01-21 Author: Claude (Session: decision_required implementation)