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

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_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):

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

  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:

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

  1. Start Both Servers:

    # Backend
    cd backend && uv run python -m app.main
    
    # Frontend
    cd frontend-sba && bun run dev
    
  2. Create New Game:

  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

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)