Generated comprehensive NEXT_SESSION.md with: - Complete analysis of Phase 3E-Final and Substitution core logic - 4 detailed tasks with code examples and acceptance criteria - Outstanding questions and architecture decisions documented - 6-7 hour estimate to complete remaining 40% Next session will complete: - WebSocket substitution events (2-3 hrs) - Validation unit tests (2 hrs) - Manager integration tests (2 hrs) - API documentation (1 hr) Phase 3 will be ~99% complete after next session. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
27 KiB
Next Session Plan - Phase 3: Substitution System Completion
Current Status: Phase 3 - 60% Substitution System Complete
Last Commit: d1619b4 - "CLAUDE: Phase 3 - Substitution System Core Logic"
Date: 2025-11-03
Remaining Work: 40% (WebSocket events, tests, documentation)
Quick Start for Next AI Agent
🎯 Where to Begin
- Read this entire document first
- Review
.claude/implementation/SUBSTITUTION_SYSTEM_SUMMARY.mdfor architecture overview - Review core logic files in "Files to Review Before Starting" section
- Start with Task 1: WebSocket Events (highest user impact)
- Run test commands after each task
📍 Current Context
We just completed the core business logic for the substitution system (1,027 lines). The validation rules, database operations, and state management are fully implemented and follow the established DB-first pattern. What remains is integration (WebSocket events for real-time gameplay) and verification (comprehensive testing).
Phase 3E (X-Check system with position ratings and Redis caching) is 100% complete. Phase 3 overall is at ~98% for X-Check work, with substitutions now being the active focus.
What We Just Completed ✅
1. Phase 3E-Final: Redis Caching & X-Check WebSocket Integration (adf7c76)
app/services/redis_client.py- Async Redis client with connection poolingapp/services/position_rating_service.py- Migrated from in-memory to Redis (760x speedup)app/main.py- Redis startup/shutdown lifecycleapp/config.py- Added redis_url settingapp/websocket/handlers.py- Enhanced submit_manual_outcome with X-Check detailsapp/websocket/X_CHECK_FRONTEND_GUIDE.md- 517 lines of frontend integration docsapp/websocket/MANUAL_VS_AUTO_MODE.md- 588 lines of workflow documentationtests/integration/test_xcheck_websocket.py- WebSocket integration tests (2 tests)test_redis_cache.py- Live Redis integration test (10 verification steps)- Performance: 760x speedup (0.274s API → 0.000361s Redis)
- Tests: 2/2 WebSocket tests passing, Redis live test validated
2. Substitution System Core Logic (d1619b4) - Just Completed
SubstitutionRules - Validation Logic (345 lines)
backend/app/core/substitution_rules.py- Complete validation for all substitution types- Methods:
validate_pinch_hitter()- Validates pinch hitter substitutionsvalidate_defensive_replacement()- Validates defensive replacementsvalidate_pitching_change()- Validates pitching changesvalidate_double_switch()- Validates complex double switches (2 simultaneous subs)
- Rules Enforced:
- No re-entry (once removed, cannot return)
- Roster eligibility (must be on roster)
- Active status (substitute must be inactive)
- Current batter check (pinch hitter only for current batter)
- Minimum batters faced (pitcher must face 1 batter unless injury)
- Position validation (valid baseball positions)
- Timing checks (warnings for mid-inning changes)
SubstitutionManager - Orchestration Logic (552 lines)
backend/app/core/substitution_manager.py- Orchestrates DB-first pattern- Methods:
pinch_hit()- Execute pinch hitter substitutiondefensive_replace()- Execute defensive replacementchange_pitcher()- Execute pitching change
- Pattern: Validate (in-memory) → Update DATABASE FIRST → Update state SECOND → Return result
- Features:
- Comprehensive error handling with error codes
- Automatic lineup cache updates
- Current player references updated (batter/pitcher/catcher in GameState)
- Detailed logging at every step
Database Operations (+115 lines)
backend/app/database/operations.py- Added substitution-specific operations- Methods:
create_substitution()- Marks old player inactive, creates new lineup entryget_eligible_substitutes()- Gets inactive players (potential subs)
- Database Fields Used:
is_active: Tracks current vs benchedis_starter: Original vs substituteentered_inning: When player enteredreplacing_id: Links to replaced playerafter_play: Exact play number of substitution
Model Enhancements (+15 lines)
backend/app/models/game_models.py- Added helper methodget_player_by_card_id()in TeamLineupState - Find player by card/player ID
3. Documentation
.claude/implementation/SUBSTITUTION_SYSTEM_SUMMARY.md- Complete architecture and status doc- Detailed implementation notes, integration points, testing strategy
Key Architecture Decisions Made
1. DB-First Pattern for Substitutions
Following established game engine pattern:
1. Validate (in-memory, fast)
2. Update DATABASE (source of truth)
3. Update STATE (cache)
4. Broadcast (WebSocket in handler)
Rationale: Database survives server restarts; in-memory state is just cache
2. Immutable Lineup History
- Never delete lineup entries
- Use
is_activeflag for current lineup replacing_idcreates complete audit trail
Benefits: Can reconstruct game at any point; rollback support; complete substitution history
3. Position Flexibility (MVP)
- Don't enforce strict position eligibility in MVP
- Any player can play any position
- Can add validation post-MVP
Rationale: Simplifies MVP; users know their rosters; reduces complexity
4. Batting Order Inheritance
- Pinch hitter takes batting order of replaced player
- Defensive replacement keeps batting order if in lineup
- Double switch allows batting order changes
Rationale: Matches real baseball rules
5. Error Codes for All Validation Failures
Every validation failure returns both human-readable message and machine-readable code:
NOT_CURRENT_BATTER,PLAYER_ALREADY_OUT,NOT_IN_ROSTER,ALREADY_ACTIVE, etc.
Rationale: Frontend can show appropriate UI based on error type
Blockers Encountered 🚧
None - development proceeded smoothly.
Core logic implementation was straightforward following established patterns. All validation rules are clear and testable. Database operations follow proven async session pattern.
Outstanding Questions ❓
1. Should we implement double switch in MVP?
Context: Double switch is complex (2 simultaneous subs with batting order swap). Core logic exists but no WebSocket events yet. Decision Needed: MVP or post-MVP? Recommendation: Post-MVP - rarely used, complex UX, single subs cover 95% of use cases
2. Position eligibility validation - when to add?
Context: Currently any player can play any position (MVP simplification). Decision Needed: Add position eligibility check post-MVP? Implication: Would require position lists on player models, more complex validation
3. Automatic pitcher fatigue system - scope?
Context: Database has is_fatigued flag on Lineup model.
Decision Needed: Implement automatic fatigue detection or manual only?
Recommendation: Manual only for MVP - managers know their staff better than algorithm
Tasks for Next Session
Task 1: WebSocket Substitution Events (2-3 hours)
File(s): backend/app/websocket/handlers.py
Goal: Add WebSocket event handlers for pinch hitting, defensive replacements, and pitching changes. Enable real-time substitution notifications to all game clients.
Changes:
- Import SubstitutionManager at top of file:
from app.core.substitution_manager import SubstitutionManager
- Initialize in GameEngine (if not already there):
# In GameEngine.__init__()
self.substitution_manager = SubstitutionManager(self.db_ops)
- Add event handler for pinch hitter:
@sio.event
async def request_pinch_hitter(sid, data):
"""
Request pinch hitter substitution.
Data:
- game_id: UUID
- player_out_lineup_id: int
- player_in_card_id: int
- team_id: int
"""
try:
game_id = UUID(data['game_id'])
# Verify user has access to game and team
# TODO: Add authorization check
result = await game_engine.substitution_manager.pinch_hit(
game_id=game_id,
player_out_lineup_id=data['player_out_lineup_id'],
player_in_card_id=data['player_in_card_id'],
team_id=data['team_id']
)
if result.success:
# Broadcast to all clients in game
await manager.emit_to_game(
game_id,
'player_substituted',
{
'type': 'pinch_hitter',
'player_out_lineup_id': result.player_out_lineup_id,
'player_in_card_id': result.player_in_card_id,
'new_lineup_id': result.new_lineup_id,
'position': result.new_position,
'batting_order': result.new_batting_order,
'team_id': data['team_id']
}
)
# Send success to requester
await sio.emit('substitution_confirmed', {
'new_lineup_id': result.new_lineup_id
}, room=sid)
else:
# Send error to requester
await sio.emit('substitution_error', {
'error': result.error_message,
'code': result.error_code
}, room=sid)
except Exception as e:
logger.error(f"Error in pinch hitter request: {e}", exc_info=True)
await sio.emit('error', {'message': str(e)}, room=sid)
-
Add similar handlers for:
request_defensive_replacement(similar structure)request_pitching_change(similar structure)
-
Add get_lineup event for UI to request current lineup:
@sio.event
async def get_lineup(sid, data):
"""Get current active lineup for a team."""
try:
game_id = UUID(data['game_id'])
team_id = data['team_id']
lineup = state_manager.get_lineup(game_id, team_id)
if lineup:
await sio.emit('lineup_data', {
'team_id': team_id,
'players': [p.model_dump() for p in lineup.players if p.is_active]
}, room=sid)
else:
await sio.emit('error', {
'message': f'Lineup not found for team {team_id}'
}, room=sid)
except Exception as e:
logger.error(f"Error getting lineup: {e}", exc_info=True)
await sio.emit('error', {'message': str(e)}, room=sid)
Files to Update:
backend/app/websocket/handlers.py- Add 4 event handlers
Test Command:
# Manual test with terminal client
python -m terminal_client
# Then in REPL, test substitution flow
Acceptance Criteria:
request_pinch_hitterevent handler implementedrequest_defensive_replacementevent handler implementedrequest_pitching_changeevent handler implementedget_lineupevent handler implemented- Successful substitutions broadcast to all clients
- Errors sent only to requester
- No crashes on invalid data
Task 2: Substitution Validation Tests (2 hours)
File(s): backend/tests/unit/core/test_substitution_rules.py (NEW)
Goal: Comprehensive unit tests for all validation rules in SubstitutionRules class.
Changes:
Create new test file with pytest structure:
"""
Unit tests for SubstitutionRules validation logic.
"""
import pytest
from uuid import uuid4
from app.core.substitution_rules import SubstitutionRules, ValidationResult
from app.models.game_models import GameState, LineupPlayerState, TeamLineupState
@pytest.fixture
def game_state():
"""Create test game state."""
state = GameState(
game_id=uuid4(),
league_id='sba',
home_team_id=1,
away_team_id=2,
current_batter_lineup_id=10
)
return state
@pytest.fixture
def roster():
"""Create test roster."""
players = [
LineupPlayerState(lineup_id=10, card_id=101, position='CF', batting_order=1, is_active=True),
LineupPlayerState(lineup_id=11, card_id=102, position='SS', batting_order=2, is_active=True),
LineupPlayerState(lineup_id=12, card_id=103, position='P', batting_order=9, is_active=True),
# Bench players
LineupPlayerState(lineup_id=20, card_id=201, position='CF', batting_order=None, is_active=False),
LineupPlayerState(lineup_id=21, card_id=202, position='SS', batting_order=None, is_active=False),
]
return TeamLineupState(team_id=1, players=players)
class TestPinchHitterValidation:
def test_success(self, game_state, roster):
"""Test successful pinch hitter validation."""
player_out = roster.get_player_by_lineup_id(10)
result = SubstitutionRules.validate_pinch_hitter(
state=game_state,
player_out=player_out,
player_in_card_id=201,
roster=roster
)
assert result.is_valid
assert result.error_message is None
def test_not_current_batter(self, game_state, roster):
"""Test pinch hit fails when not current batter."""
player_out = roster.get_player_by_lineup_id(11) # Not current batter
result = SubstitutionRules.validate_pinch_hitter(
state=game_state,
player_out=player_out,
player_in_card_id=201,
roster=roster
)
assert not result.is_valid
assert result.error_code == "NOT_CURRENT_BATTER"
def test_substitute_not_in_roster(self, game_state, roster):
"""Test pinch hit fails when substitute not in roster."""
player_out = roster.get_player_by_lineup_id(10)
result = SubstitutionRules.validate_pinch_hitter(
state=game_state,
player_out=player_out,
player_in_card_id=999, # Not in roster
roster=roster
)
assert not result.is_valid
assert result.error_code == "NOT_IN_ROSTER"
def test_substitute_already_active(self, game_state, roster):
"""Test pinch hit fails when substitute already in game."""
player_out = roster.get_player_by_lineup_id(10)
result = SubstitutionRules.validate_pinch_hitter(
state=game_state,
player_out=player_out,
player_in_card_id=102, # Already active
roster=roster
)
assert not result.is_valid
assert result.error_code == "ALREADY_ACTIVE"
class TestDefensiveReplacementValidation:
# Similar structure, test all validation paths
pass
class TestPitchingChangeValidation:
# Similar structure, test minimum batters faced, etc.
pass
Files to Create:
backend/tests/unit/core/test_substitution_rules.py(new file, ~300 lines)
Test Command:
cd backend
pytest tests/unit/core/test_substitution_rules.py -v
Acceptance Criteria:
- 15+ tests for pinch hitter validation (all validation paths)
- 12+ tests for defensive replacement validation
- 10+ tests for pitching change validation
- 5+ tests for double switch validation (optional - can defer)
- All tests passing
- Edge cases covered (player already out, not in roster, already active, etc.)
Task 3: Substitution Manager Integration Tests (2 hours)
File(s): backend/tests/integration/test_substitution_manager.py (NEW)
Goal: Integration tests for SubstitutionManager that verify full DB + state sync flow.
Changes:
Create new integration test file:
"""
Integration tests for SubstitutionManager.
Tests full flow: validation → DB → state sync
"""
import pytest
from uuid import uuid4
from app.core.substitution_manager import SubstitutionManager
from app.core.state_manager import state_manager
from app.database.operations import DatabaseOperations
from app.models.game_models import GameState
@pytest.fixture
async def setup_game():
"""Create test game with lineups in database."""
db_ops = DatabaseOperations()
game_id = uuid4()
# Create game in DB
await db_ops.create_game(
game_id=game_id,
league_id='sba',
home_team_id=1,
away_team_id=2,
game_mode='friendly',
visibility='public'
)
# Create lineup entries in DB
lineup_ids = {}
for i in range(1, 10):
lineup_id = await db_ops.add_sba_lineup_player(
game_id=game_id,
team_id=1,
player_id=100 + i,
position='P' if i == 1 else f'{i}B',
batting_order=i,
is_starter=True
)
lineup_ids[i] = lineup_id
# Add bench players
bench_ids = {}
for i in range(1, 4):
bench_id = await db_ops.add_sba_lineup_player(
game_id=game_id,
team_id=1,
player_id=200 + i,
position='OF',
batting_order=None,
is_starter=False
)
bench_ids[i] = bench_id
# Create game state
state = await state_manager.create_game(
game_id=game_id,
league_id='sba',
home_team_id=1,
away_team_id=2
)
state.current_batter_lineup_id = lineup_ids[1]
state_manager.update_state(game_id, state)
yield game_id, lineup_ids, bench_ids
# Cleanup
state_manager.remove_game(game_id)
@pytest.mark.asyncio
async def test_pinch_hit_full_flow(setup_game):
"""Test complete pinch hit flow: validation → DB → state."""
game_id, lineup_ids, bench_ids = await setup_game
db_ops = DatabaseOperations()
manager = SubstitutionManager(db_ops)
# Execute substitution
result = await manager.pinch_hit(
game_id=game_id,
player_out_lineup_id=lineup_ids[1],
player_in_card_id=201,
team_id=1
)
# Verify result
assert result.success
assert result.new_lineup_id is not None
assert result.player_out_lineup_id == lineup_ids[1]
# Verify database updated
lineup = await db_ops.get_active_lineup(game_id, team_id=1)
active_ids = [p.id for p in lineup]
assert result.new_lineup_id in active_ids
assert lineup_ids[1] not in active_ids
# Verify state updated
state = state_manager.get_state(game_id)
assert state.current_batter_lineup_id == result.new_lineup_id
# Verify lineup cache updated
roster = state_manager.get_lineup(game_id, team_id=1)
old_player = roster.get_player_by_lineup_id(lineup_ids[1])
assert not old_player.is_active
new_player = roster.get_player_by_lineup_id(result.new_lineup_id)
assert new_player.is_active
assert new_player.card_id == 201
# Add similar tests for defensive_replace and change_pitcher
Files to Create:
backend/tests/integration/test_substitution_manager.py(new file, ~400 lines)
Test Command:
cd backend
pytest tests/integration/test_substitution_manager.py -v
Acceptance Criteria:
- Test pinch_hit full flow (DB + state sync verified)
- Test defensive_replace full flow
- Test change_pitcher full flow
- Test validation failures (error handling)
- Test state recovery after substitution
- All tests passing
Task 4: Documentation - API Reference (1 hour)
File(s): .claude/implementation/SUBSTITUTION_API_REFERENCE.md (NEW)
Goal: Create comprehensive API documentation for substitution system usage.
Changes:
Create new documentation file with:
-
WebSocket Events Reference:
- Event names
- Request data format
- Response data format
- Error codes
- Example payloads
-
Python API Reference:
- SubstitutionManager methods
- SubstitutionRules methods
- Database operations
- Example code snippets
-
Workflows:
- Pinch hitter flow diagram
- Defensive replacement flow
- Pitching change flow
- Error handling patterns
-
Integration Guide:
- How to integrate with GameEngine
- How to handle substitutions in UI
- State management considerations
Files to Create:
.claude/implementation/SUBSTITUTION_API_REFERENCE.md(~300 lines)
Acceptance Criteria:
- All WebSocket events documented with examples
- All Python APIs documented
- Error codes reference table included
- Flow diagrams for each substitution type
- Integration examples provided
Files to Review Before Starting
Critical files (understand before coding):
-
backend/app/core/substitution_rules.py- All validation logic- Understand each validation method and error codes
-
backend/app/core/substitution_manager.py- Orchestration logic- Understand DB-first pattern implementation
- Note how state is updated after DB
-
backend/app/database/operations.py:290-403- DB operations- Review
create_substitution()method - Note transaction handling
- Review
-
backend/app/models/game_models.py:128-141- Model helper- Review
get_player_by_card_id()method
- Review
-
backend/app/websocket/handlers.py- Existing WebSocket patterns- Understand error handling pattern
- Note broadcast vs. emit_to_user patterns
-
.claude/implementation/SUBSTITUTION_SYSTEM_SUMMARY.md- Architecture overview- Complete context on design decisions
-
backend/app/core/game_engine.py:33-42- GameEngine initialization- See where SubstitutionManager will be integrated
Verification Steps
After completing all tasks:
1. Run all tests:
cd backend
source venv/bin/activate
# Unit tests
pytest tests/unit/core/test_substitution_rules.py -v
# Integration tests
pytest tests/integration/test_substitution_manager.py -v
# All tests
pytest -v
2. Manual testing with terminal client:
python -m terminal_client
# In REPL:
⚾ > new_game
⚾ > status # Note current batter lineup_id
⚾ > # Manual WebSocket test would go here (needs WebSocket client)
3. Verify database state:
-- Check lineup entries
SELECT id, card_id, position, batting_order, is_active, is_starter, entered_inning, replacing_id
FROM lineups
WHERE game_id = '<test_game_id>'
ORDER BY id;
-- Verify inactive players
SELECT * FROM lineups WHERE is_active = false;
4. Commit changes:
git add backend/app/websocket/handlers.py
git add backend/tests/unit/core/test_substitution_rules.py
git add backend/tests/integration/test_substitution_manager.py
git add .claude/implementation/SUBSTITUTION_API_REFERENCE.md
git commit -m "CLAUDE: Phase 3 - Complete Substitution System with WebSocket & Tests
- Add WebSocket event handlers for substitutions
- Comprehensive unit tests for validation rules (37+ tests)
- Integration tests for full DB + state flow (8+ tests)
- Complete API documentation with examples
Substitution system now 100% complete and production-ready.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>"
Success Criteria
Phase 3 Substitution System will be 100% complete when:
- 4 WebSocket event handlers implemented and tested
- 37+ unit tests passing for SubstitutionRules
- 8+ integration tests passing for SubstitutionManager
- API documentation complete with examples
- Manual testing successful via terminal client
- Database verification confirms audit trail working
- Git commit created
Overall Phase 3 Progress will be:
- Phase 3E (X-Check): 100% complete ✅
- Phase 3F (Substitutions): 100% complete ✅ (after this session)
- Phase 3 Overall: ~99% complete (only minor TODOs deferred to Phase 4+)
Quick Reference
Current Test Count: 327 tests (base), +45 expected after this session
Last Test Run: All passing (2025-11-03)
Branch: implement-phase-3
Python: 3.13.3
Virtual Env: backend/venv/
Key Imports for Next Session:
# WebSocket integration
from app.core.substitution_manager import SubstitutionManager
from app.core.state_manager import state_manager
from uuid import UUID
# Testing
import pytest
from app.core.substitution_rules import SubstitutionRules, ValidationResult
from app.models.game_models import GameState, LineupPlayerState, TeamLineupState
Recent Commit History (Last 10):
d1619b4 - CLAUDE: Phase 3 - Substitution System Core Logic (2 minutes ago)
adf7c76 - CLAUDE: Phase 3E-Final - Redis Caching & X-Check WebSocket Integration (66 minutes ago)
7d15018 - CLAUDE: Update documentation for Phase 3E-Main completion (2 hours ago)
02e816a - CLAUDE: Phase 3E-Main - Position Ratings Integration for X-Check Resolution (3 hours ago)
a55b31d - CLAUDE: Update documentation for Phase 3E-Prep completion (10 hours ago)
d560844 - CLAUDE: Phase 3E-Prep - Refactor GameState to use full LineupPlayerState objects (10 hours ago)
7417a3f - Offline catchup (11 hours ago)
683954f - CLAUDE: Update implementation notes to reflect Phase 2 completion (17 hours ago)
fc0e2f1 - CLAUDE: Integrate X-Check advancement with full GameState support (24 hours ago)
5f42576 - CLAUDE: Remove double-dipping on double play probability (24 hours ago)
Context for AI Agent Resume
If the next agent needs to understand the bigger picture:
- Overall project: See
@prd-web-scorecard-1.1.mdand@CLAUDE.md - Architecture: See
@.claude/implementation/00-index.md - Current phase details: See
@.claude/implementation/03-gameplay-features.md - Substitution system: See
@.claude/implementation/SUBSTITUTION_SYSTEM_SUMMARY.md
Critical files in current focus area:
backend/app/core/substitution_rules.py- All validation logic (345 lines)backend/app/core/substitution_manager.py- Orchestration (552 lines)backend/app/database/operations.py- DB operations (with substitution methods)backend/app/websocket/handlers.py- Will add event handlers herebackend/app/core/game_engine.py- GameEngine classbackend/app/core/state_manager.py- StateManager singletonbackend/app/models/game_models.py- Game state modelsbackend/app/models/db_models.py- Lineup model with substitution fields.claude/implementation/SUBSTITUTION_SYSTEM_SUMMARY.md- Architecture docbackend/tests/unit/core/- Where to add unit tests
What NOT to do:
- Don't modify core validation logic - it's complete and follows baseball rules correctly
- Don't change DB-first pattern - it's critical for data integrity
- Don't add position eligibility validation yet - deferred to post-MVP
- Don't implement double switch WebSocket events - deferred to post-MVP
- Don't change database schema - all fields needed are already there
Patterns we've established:
- DB-first: validate → DB → state → broadcast
- Error codes for all validation failures
- Comprehensive logging at every step
- Immutable lineup history (never delete, use is_active flag)
- Batting order inheritance for substitutions
Estimated Time for Next Session: 6-7 hours (2-3 WebSocket + 2 unit tests + 2 integration + 1 docs) Priority: Medium (completes substitution system, not blocking other work) Blocking Other Work: No (X-Check and core game engine are complete) Next Milestone After This: Phase 4 - Spectator Mode & Polish OR Phase 3 - AI Opponent (choose based on priority)