Backend:
- Add home_team_dice_color and away_team_dice_color to GameState model
- Extract dice_color from game metadata in StateManager (default: cc0000)
- Add runners_on_base param to roll_ab for chaos check skipping
Frontend - Dice Display:
- Create DiceShapes.vue with SVG d6 (square) and d20 (hexagon) shapes
- Apply home team's dice_color to d6 dice, white for resolution d20
- Show chaos d20 in amber only when WP/PB check triggered
- Add automatic text contrast based on color luminance
- Reduce blank space and remove info bubble from dice results
Frontend - Player Cards:
- Consolidate pitcher/batter cards to single location below diamond
- Add active card highlighting based on dice roll (d6_one: 1-3=batter, 4-6=pitcher)
- New card header format: [Team] Position [Name] with full card image
- Remove redundant card displays from GameBoard and GameplayPanel
- Enlarge PlayerCardModal on desktop (max-w-3xl at 1024px+)
Tests:
- Add DiceShapes.spec.ts with 34 tests for color calculations and rendering
- Update DiceRoller.spec.ts for new DiceShapes integration
- Fix test_roll_dice_success for new runners_on_base parameter
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Features:
- PlayerCardModal: Tap any player to view full playing card image
- OutcomeWizard: Progressive 3-step outcome selection (On Base/Out/X-Check)
- GameBoard: Expandable view showing all 9 fielder positions
- Post-roll card display: Shows batter/pitcher card based on d6 roll
- CurrentSituation: Tappable player cards with modal integration
Bug fixes:
- Fix batter not advancing after play (state_manager recovery logic)
- Add dark mode support for buttons and panels (partial - iOS issue noted)
New files:
- PlayerCardModal.vue, OutcomeWizard.vue, BottomSheet.vue
- outcomeFlow.ts constants for outcome category mapping
- TEST_PLAN_UI_OVERHAUL.md with 23/24 tests passing
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add player_positions JSONB column to roster_links (migration 006)
- Add player_data JSONB column to cache name/image/headshot (migration 007)
- Add is_pitcher/is_batter computed properties for two-way player support
- Update lineup submission to populate RosterLink with all players + positions
- Update get_bench handler to use cached data (no runtime API calls)
- Add BenchPlayer type to frontend with proper filtering
- Add new Lineup components: InlineSubstitutionPanel, LineupSlotRow,
PositionSelector, UnifiedLineupTab
- Add integration tests for get_bench_players
Bench players now load instantly without API dependency, and properly
filter batters vs pitchers (including CP closer position).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Backend changes:
- Modified request_game_state handler to fetch plays from database
- Convert Play DB models to frontend-compatible PlayResult dicts
- Emit game_state_sync event with state + recent_plays array
Frontend changes:
- Added deduplication by play_number in addPlayToHistory()
- Prevents duplicate plays when game_state_sync is received
Field mapping from Play model:
- hit_type -> outcome
- result_description -> description
- batter_id -> batter_lineup_id
- batter_final -> batter_result
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
New Tests:
- test_exceptions.py: Comprehensive tests for custom GameEngineError
hierarchy including AuthorizationError, DatabaseError, etc.
Scripts:
- stop-services.sh: Enhanced to kill entire process trees and
clean up orphan processes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Game Engine Improvements:
- State manager: Add game eviction with configurable max games, idle
timeout, and memory limits
- Game engine: Add resource cleanup on game completion
- Play resolver: Enhanced RunnerAdvancementData with lineup_id for
player name resolution in play-by-play
- Substitution manager: Minor improvements
Test Coverage:
- New test_game_eviction.py with 13 tests for eviction scenarios
- Updated state_manager tests for new functionality
- Updated play_resolver tests for lineup_id handling
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add rate_limit.py middleware with per-client throttling and cleanup task
- Add pool_monitor.py for database connection pool health monitoring
- Add custom exceptions module (GameEngineError, DatabaseError, etc.)
- Add config settings for eviction intervals, session timeouts, memory limits
- Add unit tests for rate limiting and pool monitoring
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixed missing PlayOutcome.HIT_BY_PITCH handling in resolve_outcome().
HBP was being submitted via WebSocket but rejected with error:
"Unhandled outcome: PlayOutcome.HIT_BY_PITCH"
Changes:
- Added HBP case to play_resolver.py (lines 430-446)
- HBP uses same forced runner advancement logic as WALK
- HBP is NOT classified as is_walk=True (correct for stats)
- Added 2 unit tests: test_hit_by_pitch, test_hit_by_pitch_bases_loaded
Test results: 981/981 passing (100%)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit fixes two critical bugs in the game engine and updates tests
to match current webhook behavior:
1. State Recovery - Batter Advancement (operations.py:545-546)
- Added missing batting_order and outs_recorded fields to plays dictionary
- These fields exist in database but weren't loaded by load_game_state()
- Root cause: Batter index was correctly recovered but current_batter remained
at placeholder (batting_order=1) because recovery logic couldn't find the
actual batting_order from last play
- Fix enables proper batter advancement after backend restarts
2. Flyball Descriptions - 2 Outs Logic (runner_advancement.py)
- Made flyball descriptions dynamic based on outs and actual base runners
- FLYOUT_A (Deep): Lines 1352-1363
- FLYOUT_B (Medium): Lines 1438-1449
- FLYOUT_BQ (Medium-shallow): Lines 1521-1530
- With 2 outs: "3rd out, inning over" (no advancement possible)
- With 0-1 outs: Dynamic based on runners ("R3 scores" only if R3 exists)
- Game logic was already correct (runs_scored=0), only descriptions were wrong
- Fixed method signatures to include hit_location parameter for all flyball methods
- Updated X-check function calls to pass hit_location parameter
3. Test Updates
- Fixed test expectation in test_flyball_advancement.py to match corrected behavior
- Descriptions now only show runners that actually exist (no phantom "R2 DECIDE")
- Auto-fixed import ordering with Ruff
- Updated websocket tests to match current webhook behavior:
* test_submit_manual_outcome_success now expects 2 broadcasts (play_resolved + game_state_update)
* test_submit_manual_outcome_missing_required_location updated to reflect hit_location now optional
Testing:
- All 739 unit tests passing (100%)
- Verified batter advances correctly after state recovery
- Verified flyball with 2 outs shows correct description
- Verified dynamic descriptions only mention actual runners on base
- Verified websocket handler broadcasts work correctly
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Removed the incorrect restriction that squeeze bunt cannot be used with bases
loaded. The only requirements for squeeze bunt are:
- Runner on third base
- Not with 2 outs
Updated validator and test to reflect correct rule.
Test results: 739/739 passing (100%)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Removed the unused alignment field from DefensiveDecision model and all
related code across backend and frontend.
Backend changes:
- models/game_models.py: Removed alignment field and validator
- terminal_client/display.py: Removed alignment from display
- core/ai_opponent.py: Updated log message
- tests/unit/models/test_game_models.py: Removed alignment tests
- tests/unit/core/test_validators.py: Removed alignment validation test
Frontend changes:
- types/game.ts: Removed alignment from DefensiveDecision interface
- components/Decisions/DefensiveSetup.vue:
* Removed alignment section from template
* Removed alignment from localSetup initialization
* Removed alignmentOptions array
* Removed alignmentDisplay computed property
* Removed alignment from hasChanges comparison
* Removed alignment from visual preview (reorganized to col-span-2)
Rationale: Defensive alignment is not active in the game and will not be
used. Per Cal's decision, remove completely rather than keep as dead code.
Tests: All 728 backend unit tests passing (100%)
Session 1 Part 3 - Change #6 complete
Part of cleanup work from demo review
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixed failing test caught by pre-commit hook. The test was not properly
mocking dependencies in the resolve_play command.
Changes:
- Added mock for state_manager.get_state() to return valid state
- Added mock for random.choice() to return deterministic PlayOutcome
- Updated assertion to expect auto-generated outcome (SINGLE_1)
- Test now properly validates the auto-outcome behavior for terminal testing
Root cause: resolve_play() checks state_manager early and auto-generates
a random outcome for testing when no forced outcome is provided. Test was
not accounting for either behavior.
All 731 unit tests now passing (100%).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Test Fixes (609/609 passing):
- Fixed DiceSystem API to accept team_id/player_id parameters for audit trails
- Fixed dice roll history timing issue in test
- Fixed terminal client mock to match resolve_play signature (X-Check params)
- Fixed result chart test mocks with missing pitching fields
- Fixed flaky test by using groundball_a (exists in both batting/pitching)
Documentation Updates:
- Added Testing Policy section to backend/CLAUDE.md
- Added Testing Policy section to tests/CLAUDE.md
- Documented 100% unit test requirement before commits
- Added git hook setup instructions
Git Hook System:
- Created .git-hooks/pre-commit script (enforces 100% test pass)
- Created .git-hooks/install-hooks.sh (easy installation)
- Created .git-hooks/README.md (hook documentation)
- Hook automatically runs all unit tests before each commit
- Blocks commits if any test fails
All 609 unit tests now passing (100%)
Integration tests have known asyncpg connection issues (documented)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Updated runner_advancement.py and its tests to use the new GameState
structure where current_batter is a LineupPlayerState object instead of
an integer ID.
Changes:
- runner_advancement.py: Replaced all 17 references to
state.current_batter_lineup_id with state.current_batter.lineup_id
- tests/unit/core/test_runner_advancement.py: Updated test fixtures
- Mock fixture: Added mock_batter object with lineup_id attribute
- GameState constructor calls: Create LineupPlayerState objects instead
of using current_batter_lineup_id parameter (9 tests fixed)
All 34 runner advancement tests now passing. This fixes the AttributeError
that was preventing X-Check resolution from completing in the terminal
client ("'GameState' object has no attribute 'current_batter_lineup_id'").
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixed state_manager._rebuild_state_from_data to provide required
current_batter field when recovering games from database. The GameState
model now requires current_batter as a LineupPlayerState object, but
recovery was not populating this field, causing validation errors.
Changes:
- state_manager.py: Create placeholder current_batter during recovery
- Build LineupPlayerState from first active batter (batting_order=1)
- Fallback to first available lineup if no #1 batter found
- Raise error if no lineups exist (invalid game state)
- _prepare_next_play() will correct the batter after recovery
- Moved get_lineup_player helper to top of method (removed duplicate)
- tests/unit/core/test_state_manager.py: Update test to use new structure
- test_update_state_nonexistent_raises_error: Create LineupPlayerState
instead of using old current_batter_lineup_id field
All 26 state_manager unit tests passing. Game recovery now works
correctly in terminal client - fixes "current_batter Field required"
validation error when running status command on recovered games.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Updated terminal client REPL to work with refactored GameState structure
where current_batter/pitcher/catcher are now LineupPlayerState objects
instead of integer IDs. Also standardized all documentation to properly
show 'uv run' prefixes for Python commands.
REPL Updates:
- terminal_client/display.py: Access lineup_id from LineupPlayerState objects
- terminal_client/repl.py: Fix typos (self.current_game → self.current_game_id)
- tests/unit/terminal_client/test_commands.py: Create proper LineupPlayerState
objects in test fixtures (2 tests fixed, all 105 terminal client tests passing)
Documentation Updates (100+ command examples):
- CLAUDE.md: Updated pytest examples to use 'uv run' prefix
- terminal_client/CLAUDE.md: Updated ~40 command examples
- tests/CLAUDE.md: Updated all test commands (unit, integration, debugging)
- app/*/CLAUDE.md: Updated test and server startup commands (5 files)
All Python commands now consistently use 'uv run' prefix to align with
project's UV migration, improving developer experience and preventing
confusion about virtual environment activation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Complete integration of position ratings system enabling X-Check defensive plays
to use actual player ratings from PD API with intelligent fallbacks for SBA.
**Live API Testing Verified**: ✅
- Endpoint: GET https://pd.manticorum.com/api/v2/cardpositions?player_id=8807
- Response: 200 OK, 7 positions retrieved successfully
- Cache performance: 16,601x faster (API: 0.214s, Cache: 0.000s)
- Data quality: Real defensive ratings (range 1-5, error 0-88)
**Architecture Overview**:
- League-aware: PD league fetches ratings from API, SBA uses defaults
- StateManager integration: Defenders retrieved from lineup cache
- Self-contained GameState: All data needed for X-Check in memory
- Graceful degradation: Falls back to league averages if ratings unavailable
**Files Created**:
1. app/services/pd_api_client.py (NEW)
- PdApiClient class for PD API integration
- Endpoint: GET /api/v2/cardpositions?player_id={id}&position={pos}
- Async HTTP client using httpx (already in requirements.txt)
- Optional position filtering: get_position_ratings(8807, ['SS', '2B'])
- Returns List[PositionRating] for all positions player can play
- Handles both list and dict response formats
- Comprehensive error handling with logging
2. app/services/position_rating_service.py (NEW)
- PositionRatingService with in-memory caching
- get_ratings_for_card(card_id, league_id) - All positions
- get_rating_for_position(card_id, position, league_id) - Specific position
- Cache performance: >16,000x faster on hits
- Singleton pattern: position_rating_service instance
- TODO Phase 3E-Final: Upgrade to Redis
3. app/services/__init__.py (NEW)
- Package exports for clean imports
4. test_pd_api_live.py (NEW)
- Live API integration test script
- Tests with real PD player 8807 (7 positions)
- Verifies caching, filtering, GameState integration
- Run: `python test_pd_api_live.py`
5. test_pd_api_mock.py (NEW)
- Mock integration test for CI/CD
- Demonstrates flow without API dependency
6. tests/integration/test_position_ratings_api.py (NEW)
- Pytest integration test suite
- Real API tests with player 8807
- Cache verification, SBA skip logic
- Full end-to-end GameState flow
**Files Modified**:
1. app/models/game_models.py
- LineupPlayerState: Added position_rating field (Optional[PositionRating])
- GameState: Added get_defender_for_position(position, state_manager)
- Uses StateManager's lineup cache to find active defender by position
- Iterates through lineup.players to match position + is_active
2. app/config/league_configs.py
- SbaConfig: Added supports_position_ratings() → False
- PdConfig: Added supports_position_ratings() → True
- Enables league-specific behavior without hardcoded conditionals
3. app/core/play_resolver.py
- __init__: Added state_manager parameter for X-Check defender lookup
- _resolve_x_check(): Replaced placeholder defender ratings with actual lookup
- Uses league config to check if ratings supported
- Fetches defender via state.get_defender_for_position()
- Falls back to defaults (range=3, error=15) if ratings unavailable
- Detailed logging for debugging rating lookups
4. app/core/game_engine.py
- Added _load_position_ratings_for_lineup() method
- Loads all position ratings at game start for PD league
- Skips loading for SBA (league config check)
- start_game(): Calls rating loader for both teams before marking active
- PlayResolver instantiation: Now passes state_manager parameter
- Logs: "Loaded X/9 position ratings for team Y"
**X-Check Resolution Flow**:
1. League check: config.supports_position_ratings()?
2. Get defender: state.get_defender_for_position(pos, state_manager)
3. If PD + defender.position_rating exists: Use actual range/error
4. Else if defender found: Use defaults (range=3, error=15)
5. Else: Log warning, use defaults
**Position Rating Loading (Game Start)**:
1. Check if league supports ratings (PD only)
2. Get lineup from StateManager cache
3. For each player:
- Fetch rating from position_rating_service (with caching)
- Set player.position_rating field
4. Cache API responses (16,000x faster on subsequent access)
5. Log success: "Loaded X/9 position ratings for team Y"
**Live Test Results (Player 8807)**:
```
Position Range Error Innings
CF 3 2 372
2B 3 8 212
SS 4 12 159
RF 2 2 74
LF 3 2 62
1B 4 0 46
3B 3 65 34
```
**Testing**:
- ✅ Live API: Player 8807 → 7 positions retrieved successfully
- ✅ Caching: 16,601x performance improvement
- ✅ League config: SBA=False, PD=True
- ✅ GameState integration: Defender lookup working
- ✅ Existing tests: 27/28 config tests passing (1 pre-existing URL failure)
- ✅ Syntax validation: All files compile successfully
**Benefits**:
- ✅ X-Check now uses real defensive ratings in PD league
- ✅ SBA league continues working with manual entry (uses defaults)
- ✅ No breaking changes to existing functionality
- ✅ Graceful degradation if API unavailable
- ✅ In-memory caching reduces API calls by >99%
- ✅ League-agnostic design via config system
- ✅ Production-ready with live API verification
**Phase 3E Status**: Main complete (85% → 90%)
**Next**: Phase 3E-Final (WebSocket events, Redis upgrade, full defensive lineup)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
**Architectural Improvement**: Unified player references in GameState
**Changed**: Make all player references consistent
- BEFORE: current_batter/pitcher/catcher were IDs (int)
- AFTER: current_batter/pitcher/catcher are full LineupPlayerState objects
- Matches pattern of on_first/on_second/on_third (already objects)
**Benefits**:
1. Consistent API - all player references use same type
2. Self-contained GameState - everything needed for resolution
3. No lookups needed - direct access to player data
4. Sets foundation for Phase 3E-Main (adding position ratings)
**Files Modified**:
- app/models/game_models.py: Changed current_batter/pitcher/catcher to objects
- app/core/game_engine.py: Updated _prepare_next_play() to populate full objects
- app/core/state_manager.py: Create placeholder batter on game creation
- tests/unit/models/test_game_models.py: Updated all 27 GameState tests
**Database Operations**:
- No schema changes needed
- Play table still stores IDs (for referential integrity)
- IDs extracted from objects when saving: state.current_batter.lineup_id
**Testing**:
- All 27 GameState tests passing
- No regressions in existing functionality
- Type checking passes
**Next**: Phase 3E-Main - Add PositionRating dataclass and load ratings at game start
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Updated X-Check runner advancement functions to properly delegate to
existing result handlers for non-error cases.
Changes:
- Updated x_check_g1/g2/g3 signatures to accept GameState, hit_location,
and defensive_decision parameters
- Updated x_check_f1/f2/f3 signatures to accept GameState and hit_location
- Implemented delegation logic: error cases use simple tables, non-error
cases delegate to existing tested result handlers (_execute_result,
_fb_result_*)
- Updated PlayResolver._get_x_check_advancement() to pass new parameters
- Updated all tests to provide required GameState fixtures
Benefits:
- Reuses 13 existing groundball + 4 flyball result handlers (DRY)
- No DP probability needed - X-Check d20 already tested defender
- Full game context: real lineup IDs, outs count, conditional logic
- Error cases remain simple and efficient
Test Results: 264/265 core tests passing (1 pre-existing dice failure)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixed incorrect double play logic that was rolling for probability
twice - once for the chart result and again for execution.
Changes:
- Removed _calculate_double_play_probability() method entirely
- Updated _gb_result_2() to execute DP deterministically
- Updated _gb_result_10() to execute DP deterministically
- Updated _gb_result_13() to execute DP deterministically
- Removed TestDoublePlayProbability test class (5 tests)
- Updated DP tests to reflect deterministic behavior
Logic: Chart already determines outcome via dice roll. When chart
says "Result 2: Double Play", the DP happens (if <2 outs and runner
on 1st exists). No additional probability roll needed.
Tests: 55/55 runner advancement tests passing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixed two critical bugs in Phase 3D X-Check implementation plus
improved dice audit trail for better tracking.
BUG #1: on_base_code Mapping Error (Sequential vs Bit Field)
============================================================
The implementation incorrectly treated on_base_code as a bit field
when it is actually a sequential lookup mapping.
WRONG (bit field):
Code 3 (0b011) → R1 + R2
Code 4 (0b100) → R3 only
CORRECT (sequential):
Code 3 → R3 only
Code 4 → R1 + R2
Fixed:
- build_advancement_from_code() decoder (sequential mapping)
- build_flyball_advancement_with_error() decoder (sequential mapping)
- 13 test on_base_code values (3↔4 corrections)
- Updated documentation to clarify NOT a bit field
BUG #2: Table Data Not Matching Official Charts
================================================
7 table entries in G1_ADVANCEMENT_TABLE and G2_ADVANCEMENT_TABLE
did not match the official rulebook charts provided by user.
Fixed table entries:
- G1 Code 1, Infield In: Changed Result 3 → 2
- G1 Code 3, Normal: Changed Result 13 → 3
- G1 Code 3, Infield In: Changed Result 3 → 1
- G1 Code 4, Normal: Changed Result 3 → 13
- G1 Code 4, Infield In: Changed Result 4 → 2
- G2 Code 3, Infield In: Changed Result 3 → 1
- G2 Code 4, Normal: Changed Result 5 → 4
Also fixed 7 test expectations to match corrected tables.
IMPROVEMENT: Better Dice Audit Trail
=====================================
Updated _resolve_x_check() in PlayResolver to use proper
dice_system.roll_fielding() instead of manual die rolling.
Benefits:
- All dice tracked in audit trail (roll_id, timestamp, position)
- Automatic error_total calculation (no manual 3d6 addition)
- Consistent with codebase patterns
- Position recorded for historical analysis
Testing:
- All 59 X-Check advancement tests passing (100%)
- All 9 PlayResolver tests passing (100%)
- All table entries validated against official charts
- Complete codebase scan: no bit field operations found
Files modified:
- backend/app/core/x_check_advancement_tables.py
- backend/tests/unit/core/test_x_check_advancement_tables.py
- backend/app/core/play_resolver.py
- .claude/implementation/PHASE_3D_CRITICAL_FIX.md (documentation)
- .claude/implementation/GROUNDBALL_CHART_REFERENCE.md (new)
- .claude/implementation/XCHECK_TEST_VALIDATION.md (new)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Major Phase 2 refactoring to consolidate runner advancement logic:
**Flyball System Enhancement**:
- Add FLYOUT_BQ variant (medium-shallow depth)
- 4 flyball types with clear semantics: A (deep), B (medium), BQ (medium-shallow), C (shallow)
- Updated helper methods to include FLYOUT_BQ
**RunnerAdvancement Integration**:
- Extend runner_advancement.py to handle both groundballs AND flyballs
- advance_runners() routes to _advance_runners_groundball() or _advance_runners_flyball()
- Comprehensive flyball logic with proper DECIDE mechanics per flyball type
- No-op movements recorded for state recovery consistency
**PlayResolver Refactoring**:
- Consolidate all 4 flyball outcomes to delegate to RunnerAdvancement (DRY)
- Eliminate duplicate flyball resolution code
- Rename helpers for clarity: _advance_on_single_1/_advance_on_single_2 (was _advance_on_single)
- Fix single/double advancement logic for different hit types
**State Recovery Fix**:
- Fix state_manager.py game recovery to build LineupPlayerState objects properly
- Use get_lineup_player() helper to construct from lineup data
- Correctly track runners in on_first/on_second/on_third fields (matches Phase 2 model)
**Database Support**:
- Add runner tracking fields to play data for accurate recovery
- Include batter_id, on_first_id, on_second_id, on_third_id, and *_final fields
**Type Safety Improvements**:
- Fix lineup_id access throughout runner_advancement.py (was accessing on_first directly, now on_first.lineup_id)
- Make current_batter_lineup_id non-optional (always set by _prepare_next_play)
- Add type: ignore for known SQLAlchemy false positives
**Documentation**:
- Update CLAUDE.md with comprehensive flyball documentation
- Add flyball types table, usage examples, and test coverage notes
- Document differences between groundball and flyball mechanics
**Testing**:
- Add test_flyball_advancement.py with 21 flyball tests
- Coverage: all 4 types, DECIDE scenarios, no-op movements, edge cases
🚀 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Update test fixtures to provide required current_batter_lineup_id field
- Update validator tests for new infield depth values (infield_in, normal, corners_in)
- Update runner advancement tests to match refactored runner management
- Update game model tests to work with direct base references
- Update play resolver tests for enhanced logic
- Add missing imports in test files
All changes ensure tests align with recent Phase 2 implementation updates
including the transition from RunnerState list to direct base references
(on_first, on_second, on_third) in GameState model.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Major Refactor: Outcome-First Architecture
- PlayResolver now accepts league_id and auto_mode in constructor
- Added core resolve_outcome() method - all resolution logic in one place
- Added resolve_manual_play() wrapper for manual submissions (primary)
- Added resolve_auto_play() wrapper for PD auto mode (rare)
- Removed SimplifiedResultChart (obsolete with new architecture)
- Removed play_resolver singleton
RunnerAdvancement Integration:
- All groundball outcomes (GROUNDBALL_A/B/C) now use RunnerAdvancement
- Proper DP probability calculation with positioning modifiers
- Hit location tracked for all relevant outcomes
- 13 result types fully integrated from advancement charts
Game State Updates:
- Added auto_mode field to GameState (stored per-game)
- Updated state_manager.create_game() to accept auto_mode parameter
- GameEngine now uses state.auto_mode to create appropriate resolver
League Configuration:
- Added supports_auto_mode() to BaseGameConfig
- SbaConfig: returns False (no digitized cards)
- PdConfig: returns True (has digitized ratings)
- PlayResolver validates auto mode support and raises error for SBA
Play Results:
- Added hit_location field to PlayResult
- Groundballs include location from RunnerAdvancement
- Flyouts track hit_location for tag-up logic (future)
- Other outcomes have hit_location=None
Testing:
- Completely rewrote test_play_resolver.py for new architecture
- 9 new tests covering initialization, strikeouts, walks, groundballs, home runs
- All 9 tests passing
- All 180 core tests still passing (1 pre-existing failure unrelated)
Terminal Client:
- No changes needed - defaults to manual mode (auto_mode=False)
- Perfect for human testing of manual submissions
This completes Week 7 Task 6 - the final task of Week 7!
Week 7 is now 100% complete with all 8 tasks done.
🎯 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Created runner_advancement.py with complete groundball advancement system
- Implemented GroundballResultType IntEnum with 13 rulebook-aligned results
- Built RunnerAdvancement class with chart lookup logic (Infield Back/In)
- Implemented all 13 result handlers (gb_result_1 through gb_result_13)
- Added DECIDE mechanic support for interactive runner advancement decisions
- Implemented double play probability calculation with positioning modifiers
- Created 30 comprehensive unit tests covering all scenarios (100% passing)
Key Features:
- Supports Infield Back and Infield In defensive positioning
- Handles Corners In hybrid positioning (applies In rules to corner fielders)
- Conditional results based on hit location (middle IF, right side, corners)
- Force play detection and advancement logic
- Double play mechanics with probability-based success (45% base rate)
- Result types match official rulebook exactly (1-13)
Architecture:
- IntEnum for result types (type-safe, self-documenting)
- Comprehensive hit location tracking (1B, 2B, SS, 3B, P, C, LF, CF, RF)
- Dataclasses for movements (RunnerMovement, AdvancementResult)
- Probability modifiers: Infield In (-15%), hit location (±10%)
Testing:
- 30 unit tests covering chart lookup, all result types, and edge cases
- Double play probability validation
- All on-base codes (0-7) tested
- All groundball types (A, B, C) verified
Status: Week 7 Tasks 4 & 5 complete (~87% of Week 7 finished)
Next: Task 6 (PlayResolver Integration)
Related: #task4 #task5 #runner-advancement #double-play #week7
Complete manual outcome workflow for SBA and PD manual mode gameplay:
**WebSocket Event Handlers** (app/websocket/handlers.py):
- roll_dice: Server rolls dice, stores in state, broadcasts to players
- submit_manual_outcome: Validates and processes player submissions
- Events: dice_rolled, outcome_accepted, outcome_rejected, play_resolved
**Game Engine Integration** (app/core/game_engine.py):
- resolve_manual_play(): Processes manual outcomes with server dice
- Uses ab_roll for audit trail, player outcome for resolution
- Same orchestration as resolve_play() (save, update, advance inning)
**Data Model** (app/models/game_models.py):
- pending_manual_roll: Stores server dice between roll and submission
**Terminal Client** (terminal_client/):
- roll_dice command: Roll dice and display results
- manual_outcome command: Submit outcomes from physical cards
- Both integrated into REPL for testing
**Tests** (tests/unit/websocket/test_manual_outcome_handlers.py):
- 12 comprehensive tests covering all validation paths
- All tests passing (roll_dice: 4, submit_manual_outcome: 8)
**Key Decisions**:
- Server rolls dice for fairness (not players!)
- One-time roll usage (cleared after submission)
- Early validation (check pending roll before accepting)
- Field-level error messages for clear feedback
**Impact**:
- Complete manual mode workflow ready
- Frontend WebSocket integration supported
- Terminal testing commands available
- Audit trail with server-rolled dice maintained
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Enhanced validate_defensive_decision() with comprehensive validation:
- Validate all alignments (normal, shifted_left, shifted_right, extreme_shift)
- Validate all infield depths (in, normal, back, double_play)
- Validate all outfield depths (in, normal, back)
- Validate hold_runners require actual runners on specified bases
- Validate hold_runners only on bases 1, 2, or 3
- Validate double_play depth requires runner on first
- Validate double_play depth not allowed with 2 outs
- Enhanced validate_offensive_decision() with comprehensive validation:
- Validate all approaches (normal, contact, power, patient)
- Validate steal_attempts only to bases 2, 3, or 4
- Validate steal_attempts require runner on base-1
- Validate bunt_attempt not allowed with 2 outs
- Validate bunt_attempt and hit_and_run cannot be simultaneous
- Validate hit_and_run requires at least one runner on base
- Added 24+ comprehensive test cases covering all edge cases:
- 13 new defensive decision validation tests
- 16 new offensive decision validation tests
- All tests pass (54/54 passing)
Clear error messages for all validation failures.
Follows 'Raise or Return' pattern with ValidationError exceptions.
- Renamed check_d20 → chaos_d20 throughout dice system
- Expanded PlayOutcome enum with granular variants (SINGLE_1/2, DOUBLE_2/3, GROUNDBALL_A/B/C, etc.)
- Integrated PlayOutcome from app.config into PlayResolver
- Added play_metadata support for uncapped hit tracking
- Updated all tests (139/140 passing)
Week 6: 100% Complete - Ready for Phase 3
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit includes cleanup from model refactoring and terminal client
modularization for better code organization and maintainability.
## Game Models Refactor
**Removed RunnerState class:**
- Eliminated separate RunnerState model (was redundant)
- Replaced runners: List[RunnerState] with direct base references:
- on_first: Optional[LineupPlayerState]
- on_second: Optional[LineupPlayerState]
- on_third: Optional[LineupPlayerState]
- Updated helper methods:
- get_runner_at_base() now returns LineupPlayerState directly
- get_all_runners() returns List[Tuple[int, LineupPlayerState]]
- is_runner_on_X() simplified to direct None checks
**Benefits:**
- Matches database structure (plays table has on_first_id, etc.)
- Simpler state management (direct references vs list management)
- Better type safety (LineupPlayerState vs generic runner)
- Easier to work with in game engine logic
**Updated files:**
- app/models/game_models.py - Removed RunnerState, updated GameState
- app/core/play_resolver.py - Use get_all_runners() instead of state.runners
- app/core/validators.py - Updated runner access patterns
- tests/unit/models/test_game_models.py - Updated test assertions
- tests/unit/core/test_play_resolver.py - Updated test data
- tests/unit/core/test_validators.py - Updated test data
## Terminal Client Refactor
**Modularization (DRY principle):**
Created separate modules for better code organization:
1. **terminal_client/commands.py** (10,243 bytes)
- Shared command functions for game operations
- Used by both CLI (main.py) and REPL (repl.py)
- Functions: submit_defensive_decision, submit_offensive_decision,
resolve_play, quick_play_sequence
- Single source of truth for command logic
2. **terminal_client/arg_parser.py** (7,280 bytes)
- Centralized argument parsing and validation
- Handles defensive/offensive decision arguments
- Validates formats (alignment, depths, hold runners, steal attempts)
3. **terminal_client/completions.py** (10,357 bytes)
- TAB completion support for REPL mode
- Command completions, option completions, dynamic completions
- Game ID completions, defensive/offensive option suggestions
4. **terminal_client/help_text.py** (10,839 bytes)
- Centralized help text and command documentation
- Detailed command descriptions
- Usage examples for all commands
**Updated main modules:**
- terminal_client/main.py - Simplified by using shared commands module
- terminal_client/repl.py - Cleaner with shared functions and completions
**Benefits:**
- DRY: Behavior consistent between CLI and REPL modes
- Maintainability: Changes in one place affect both interfaces
- Testability: Can test commands module independently
- Organization: Clear separation of concerns
## Documentation
**New files:**
- app/models/visual_model_relationships.md
- Visual documentation of model relationships
- Helps understand data flow between models
- terminal_client/update_docs/ (6 phase documentation files)
- Phased documentation for terminal client evolution
- Historical context for implementation decisions
## Tests
**New test files:**
- tests/unit/terminal_client/__init__.py
- tests/unit/terminal_client/test_arg_parser.py
- tests/unit/terminal_client/test_commands.py
- tests/unit/terminal_client/test_completions.py
- tests/unit/terminal_client/test_help_text.py
**Updated tests:**
- Integration tests updated for new runner model
- Unit tests updated for model changes
- All tests passing with new structure
## Summary
- ✅ Simplified game state model (removed RunnerState)
- ✅ Better alignment with database structure
- ✅ Modularized terminal client (DRY principle)
- ✅ Shared command logic between CLI and REPL
- ✅ Comprehensive test coverage
- ✅ Improved documentation
Total changes: 26 files modified/created
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit includes Week 6 player models implementation and critical
performance optimizations discovered during testing.
## Player Models (Week 6 - 50% Complete)
**New Files:**
- app/models/player_models.py (516 lines)
- BasePlayer abstract class with polymorphic interface
- SbaPlayer with API parsing factory method
- PdPlayer with batting/pitching scouting data support
- Supporting models: PdCardset, PdRarity, PdBattingCard, PdPitchingCard
- tests/unit/models/test_player_models.py (692 lines)
- 32 comprehensive unit tests, all passing
- Tests for BasePlayer, SbaPlayer, PdPlayer, polymorphism
**Architecture:**
- Simplified single-layer approach vs planned two-layer
- Factory methods handle API → Game transformation directly
- SbaPlayer.from_api_response(data) - parses SBA API inline
- PdPlayer.from_api_response(player_data, batting_data, pitching_data)
- Full Pydantic validation, type safety, and polymorphism
## Performance Optimizations
**Database Query Reduction (60% fewer queries per play):**
- Before: 5 queries per play (INSERT play, SELECT play with JOINs,
SELECT games, 2x SELECT lineups)
- After: 2 queries per play (INSERT play, UPDATE games conditionally)
Changes:
1. Lineup caching (game_engine.py:384-425)
- Check state_manager.get_lineup() cache before DB fetch
- Eliminates 2 SELECT queries per play
2. Remove unnecessary refresh (operations.py:281-302)
- Removed session.refresh(play) after INSERT
- Eliminates 1 SELECT with 3 expensive LEFT JOINs
3. Direct UPDATE statement (operations.py:109-165)
- Changed update_game_state() to use direct UPDATE
- No longer does SELECT + modify + commit
4. Conditional game state updates (game_engine.py:200-217)
- Only UPDATE games table when score/inning/status changes
- Captures state before/after and compares
- ~40-60% fewer updates (many plays don't score)
## Bug Fixes
1. Fixed outs_before tracking (game_engine.py:551)
- Was incorrectly calculating: state.outs - result.outs_recorded
- Now correctly captures: state.outs (before applying result)
- All play records now have accurate out counts
2. Fixed game recovery (state_manager.py:312-314)
- AttributeError when recovering: 'GameState' has no attribute 'runners'
- Changed to use state.get_all_runners() method
- Games can now be properly recovered from database
## Enhanced Terminal Client
**Status Display Improvements (terminal_client/display.py:75-97):**
- Added "⚠️ WAITING FOR ACTION" section when play is pending
- Shows specific guidance:
- "The defense needs to submit their decision" → Run defensive [OPTIONS]
- "The offense needs to submit their decision" → Run offensive [OPTIONS]
- "Ready to resolve play" → Run resolve
- Color-coded command hints for better UX
## Documentation Updates
**backend/CLAUDE.md:**
- Added comprehensive Player Models section (204 lines)
- Updated Current Phase status to Week 6 (~50% complete)
- Documented all optimizations and bug fixes
- Added integration examples and usage patterns
**New Files:**
- .claude/implementation/week6-status-assessment.md
- Comprehensive Week 6 progress review
- Architecture decision rationale (single-layer vs two-layer)
- Completion status and next priorities
- Updated roadmap for remaining Week 6 work
## Test Results
- Player models: 32/32 tests passing
- All existing tests continue to pass
- Performance improvements verified with terminal client
## Next Steps (Week 6 Remaining)
1. Configuration system (BaseConfig, SbaConfig, PdConfig)
2. Result charts & PD play resolution with ratings
3. API client for live roster data (deferred)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add comprehensive unit and integration tests for Week 5 deliverables:
- test_play_resolver.py: 18 tests covering outcome resolution and runner advancement
- test_validators.py: 36 tests covering game state, decisions, lineups, and flow
- test_game_engine.py: 7 test classes for complete game flow integration
Update implementation documentation to reflect completed status:
- 00-index.md: Mark Phase 2 Weeks 4-5 complete with test coverage
- 02-week5-game-logic.md: Comprehensive test details and completion status
- 02-game-engine.md: Forward-looking snapshot pattern documentation
Week 5 now fully complete with 54 unit tests + 7 integration test classes passing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Attempted Fix:
- Created test-specific engine with NullPool
- Monkeypatched DatabaseOperations to use test engine
- Reference: https://github.com/MagicStack/asyncpg/issues/863
Result:
❌ NullPool did NOT resolve the issue
- Tests still fail after #4 with "another operation is in progress"
- Error occurs during fixture setup, not in test bodies
- Timestamps show pytest setting up multiple fixtures concurrently
Root Cause Analysis:
The issue isn't connection pooling - it's async fixture dependency chains.
When pytest-asyncio sets up `sample_game` fixture (which uses `db_ops`),
it creates overlapping async contexts that asyncpg can't handle.
Evidence:
- Individual tests: ✅ PASS
- First 4 tests together: ✅ PASS
- Tests 5-16: ❌ FAIL with concurrent operation errors
- Unit tests: ✅ 87/88 PASS (core logic proven correct)
Conclusion:
This is a complex pytest-asyncio + SQLAlchemy + asyncpg interaction
requiring architectural test changes (separate test DB, sync fixtures, etc).
Not worth solving pre-MVP given tests work individually and code is proven.
Workaround:
Run test classes separately - each class passes fine:
pytest tests/integration/database/test_roll_persistence.py::TestRollPersistenceBatch -v
pytest tests/integration/database/test_roll_persistence.py::TestRollRetrieval -v
pytest tests/integration/database/test_roll_persistence.py::TestRollDataIntegrity -v
pytest tests/integration/database/test_roll_persistence.py::TestRollEdgeCases -v
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>