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>
This commit is contained in:
Cal Corum 2025-11-21 15:41:16 -06:00
parent 1373286391
commit f3eb5e8200
3 changed files with 2504 additions and 0 deletions

View File

@ -0,0 +1,335 @@
# 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)

View File

@ -0,0 +1,338 @@
# Gameplay Loop Implementation - Session Handoff
**Date**: 2025-01-21
**Session Focus**: Connecting frontend gameplay UI to backend WebSocket handlers
**Status**: 🟡 In Progress - Defensive decisions working, testing offensive workflow next
---
## 🎯 Session Goals
Implement the core gameplay loop: dice rolling → outcome submission → play resolution
**Target Workflow**:
1. User submits defensive setup (infield/outfield positioning)
2. User submits offensive decision (batting action)
3. User rolls dice
4. User submits manual outcome (from physical card)
5. Backend resolves play and broadcasts result
6. Game state updates, play appears in feed
---
## ✅ What We Accomplished
### 1. **Connected Gameplay Loop Components**
- Fixed `submitManualOutcome()` signature in `useGameActions.ts`
- Added `play_resolved` WebSocket event handler
- Fixed game page to call outcome submission correctly
- Added missing TypeScript imports
### 2. **Fixed Decision Panel Visibility Issues**
**Root Cause**: Frontend was checking for `decision_phase: "defense"` but backend uses `"awaiting_defensive"`
**Files Modified**:
- `frontend-sba/store/game.ts` - Added checks for both naming conventions:
```typescript
needsDefensiveDecision: checks 'defense' OR 'awaiting_defensive'
needsOffensiveDecision: checks 'offensive_approach' OR 'awaiting_offensive'
```
### 3. **Fixed Game Start Issues**
**Problem**: Games were stuck in `decision_phase: "idle"` after lineups submitted
**Root Causes Fixed**:
1. Missing `game_engine` import in `backend/app/api/routes/games.py`
2. `start_game()` wasn't setting `decision_phase` after preparing first play
**Files Modified**:
- `backend/app/api/routes/games.py`:
```python
from app.core.game_engine import game_engine # Added import
```
- `backend/app/core/game_engine.py`:
```python
# In start_game() after _prepare_next_play():
state.decision_phase = "awaiting_defensive" # Added this line
```
### 4. **Fixed State Updates After Decision Submission**
**Problem**: After submitting defensive decision, UI stayed on defensive panel instead of advancing to offensive
**Solution**: Frontend now requests updated game state after decision submissions
**File Modified**:
- `frontend-sba/composables/useWebSocket.ts`:
```typescript
socketInstance.on('defensive_decision_submitted', (data) => {
// Request updated game state to get new decision_phase
socketInstance.emit('request_game_state', { game_id: gameStore.gameId })
})
```
### 5. **Added Debug Logging**
- Added console logging in game page to debug panel visibility
- Logs show `decision_phase`, `needsDefensiveDecision`, `needsOffensiveDecision`, etc.
---
## 📁 Files Modified (Need Committing)
### Backend Changes
```
backend/app/api/routes/games.py
backend/app/core/game_engine.py
```
### Frontend Changes
```
frontend-sba/store/game.ts
frontend-sba/composables/useGameActions.ts
frontend-sba/composables/useWebSocket.ts
frontend-sba/pages/games/[id].vue
```
---
## ✅ What's Working Now
### ✓ Game Creation Flow
- Create game → submit lineups → game auto-starts
- Game enters `"awaiting_defensive"` phase immediately
- Frontend shows **Defensive Setup** panel
### ✓ Defensive Decision Submission
- User can select infield/outfield positioning
- Clicking "Submit" sends decision to backend
- Backend receives and processes decision
- Emits `defensive_decision_submitted` event
- Frontend requests updated game state
### ✓ WebSocket Communication
- Connection established successfully
- Events flowing backend → frontend
- `dice_rolled`, `outcome_accepted`, `play_resolved` handlers implemented
### ✓ Backend Game Engine
- All 15 WebSocket handlers implemented (Phase 3E-Final)
- Manual outcome workflow complete
- Play resolution working
- Database persistence functional
---
## 🔄 Current State
### What User Just Did
1. ✅ Created new game (ID: `0411d0ce-f41f-46eb-b0ac-23bae818d6ad`)
2. ✅ Saw defensive setup panel appear
3. ✅ Submitted defensive decision
4. ⚠️ Still seeing "Your Defensive Turn" indicator
### What Should Happen Next
After latest fix (requesting game state after submission), user should:
1. Refresh game page
2. Submit defensive decision again
3. Should see **Offensive Approach** panel appear
4. Select batting action → Submit
5. Should see **Gameplay Panel** with **Roll Dice** button
---
## 🧪 Testing Checklist
### ✅ Tested & Working
- [x] Game creation
- [x] Lineup submission
- [x] Game auto-start
- [x] Defensive panel appears
- [x] Defensive decision sends to backend
### ⏳ Next to Test
- [ ] Defensive submission → Offensive panel transition
- [ ] Offensive decision submission
- [ ] Offensive → Resolution phase (dice rolling)
- [ ] Dice roll button click
- [ ] Dice results display
- [ ] Manual outcome entry
- [ ] Outcome submission
- [ ] Play resolution display
- [ ] Play-by-play feed update
- [ ] Game state updates (score, outs, runners)
- [ ] Second at-bat (loop continues)
---
## 🔧 Technical Details
### Backend Decision Phase Values
```python
"idle" # Initial state (before start)
"awaiting_defensive" # Waiting for defensive setup
"awaiting_offensive" # Waiting for offensive approach
"resolution" # Ready for dice rolling
```
### WebSocket Event Flow
**Decision Submission**:
```
Frontend: submit_defensive_decision →
Backend: process → emit defensive_decision_submitted →
Frontend: request_game_state →
Backend: game_state (with decision_phase: "awaiting_offensive") →
Frontend: Show offensive panel
```
**Dice Rolling & Outcome**:
```
Frontend: roll_dice →
Backend: roll dice → emit dice_rolled →
Frontend: Show dice results →
User: Read card, select outcome →
Frontend: submit_manual_outcome →
Backend: validate & resolve → emit play_resolved →
Frontend: Display result, update state
```
### Key Store Properties
```typescript
// Game Store (frontend-sba/store/game.ts)
needsDefensiveDecision // Shows defensive panel
needsOffensiveDecision // Shows offensive panel
canRollDice // Enables dice roll button
canSubmitOutcome // Enables outcome submission
```
---
## 🐛 Known Issues & Workarounds
### Issue 1: Frontend Compilation Warnings
**Symptom**: "Element is missing end tag" errors in console
**Impact**: None - server still runs fine
**Status**: Cosmetic, doesn't affect functionality
### Issue 2: Old Games Stuck in "idle"
**Symptom**: Games created before our fixes still show "Waiting for strategic decisions"
**Workaround**: Create fresh games - old games won't auto-advance
**Fix**: Old games need manual state update or recreation
---
## 🚀 Quick Start for Next Session
### 1. Check Server Status
```bash
# Backend should be running on port 8000
# Frontend should be running on port 3001
```
### 2. Test Defensive → Offensive Transition
1. Go to current game: `http://localhost:3001/games/0411d0ce-f41f-46eb-b0ac-23bae818d6ad`
2. Submit defensive decision
3. **Should** see offensive panel appear now (after latest fix)
### 3. If Offensive Panel Appears - Continue Testing!
- Submit offensive decision
- Click "Roll Dice"
- Check dice results display
- Submit outcome
- Verify play resolution
### 4. If Still Stuck - Create Fresh Game
```
http://localhost:3001/games/create
```
New games will have all fixes applied.
---
## 📝 Pending Commits
### Gameplay Loop Connection (Already Committed)
```bash
git log --oneline -1
# 58b5deb CLAUDE: Connect gameplay loop - dice rolling and play resolution
```
### Decision Panel Fixes (Need Committing)
```bash
# Backend changes
backend/app/api/routes/games.py # Added game_engine import
backend/app/core/game_engine.py # Set decision_phase in start_game
# Frontend changes
frontend-sba/store/game.ts # Check both decision_phase naming conventions
frontend-sba/composables/useWebSocket.ts # Request state after decision submissions
frontend-sba/pages/games/[id].vue # Added debug logging
```
---
## 🎓 Lessons Learned
### 1. Backend/Frontend Contract Mismatches
**Issue**: Backend used "awaiting_defensive" but frontend checked for "defense"
**Solution**: Frontend now checks both variants for robustness
### 2. State Synchronization
**Issue**: Backend updated state but didn't broadcast changes
**Solution**: Frontend explicitly requests updated state after key events
### 3. Game Initialization
**Issue**: `start_game()` prepared play but didn't set decision_phase
**Solution**: Explicitly set initial decision_phase after prepare
### 4. Import Issues Can Fail Silently
**Issue**: Missing `game_engine` import caused silent failure with try/catch
**Solution**: Always verify imports are present and check logs for errors
---
## 📞 If You Get Stuck
### Check These First
1. **Backend logs**: `BashOutput` with filter for game ID or event names
2. **Browser console**: Look for `[Game Page] Panel visibility check:` log
3. **WebSocket events**: Check if events are being received
4. **Game state**: Look at `decision_phase` value in console log
### Common Fixes
- **Panels not showing**: Check `decision_phase` value, create fresh game
- **Events not working**: Check WebSocket connection status
- **State not updating**: Look for `game_state` or `game_state_update` events
---
## 🎯 Success Criteria
**Session Complete When**:
- [x] Defensive panel appears ✅
- [ ] Offensive panel appears after defensive submission
- [ ] Dice can be rolled
- [ ] Outcome can be submitted
- [ ] Play resolves and appears in feed
- [ ] Second at-bat starts automatically
- [ ] All changes committed
**Current Progress**: ~60% complete
---
## 📚 References
- **Backend Handlers**: `backend/app/websocket/handlers.py` (15 handlers, all implemented)
- **Game Engine**: `backend/app/core/game_engine.py` (Phase 3E-Final complete)
- **Frontend Components**: `frontend-sba/components/Gameplay/` (all built, partially connected)
- **Main Session Context**: `CLAUDE.md` in project root
---
**Next Session Goal**: Complete offensive decision → dice rolling → outcome submission flow
Good luck! 🚀

File diff suppressed because it is too large Load Diff