Issue #4 from code review - multi-step database operations were not
wrapped in transactions, risking partial state on failure.
Changes:
- Modified save_play() and update_game_state() in DatabaseOperations
to accept optional session parameter for transaction grouping
- Wrapped resolve_play() DB operations in single atomic transaction
- Wrapped resolve_manual_play() DB operations in single atomic transaction
- Transaction commits atomically or rolls back entirely on failure
Pattern: When session provided, use flush() for IDs without committing;
caller controls transaction. When no session, create internal transaction.
All 739 unit tests passing.
🤖 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>
Add ability to roll back the last N plays, useful for correcting mistakes
or recovering from corrupted plays. Deletes plays from database and
reconstructs game state by replaying remaining plays.
Database Operations (app/database/operations.py):
- delete_plays_after(): Delete plays with play_number > target
- delete_substitutions_after(): Delete lineup entries with after_play >= target
- delete_rolls_after(): Delete dice rolls (kept for reference, not used)
Game Engine (app/core/game_engine.py):
- rollback_plays(): Main rollback orchestration
- Validates: num_plays > 0, enough plays exist, game not completed
- Deletes plays and substitutions from database
- Clears in-memory roll tracking
- Calls state_manager.recover_game() to rebuild state
- Returns updated GameState
Terminal Client (terminal_client/commands.py, terminal_client/repl.py):
- rollback_plays(): Command wrapper with user-friendly output
- do_rollback(): REPL command with argument parsing
Usage:
⚾ > rollback 3
Validations:
- Cannot roll back more plays than exist
- Cannot roll back completed games
- Rolling back across innings is allowed
- Substitutions after rolled-back plays are undone
Testing:
- ✅ Successfully rolls back 2 plays from 5-play game
- ✅ Correctly validates rollback of 10 plays when only 2 exist
- ✅ Game state properly reconstructed via replay
Note: Dice rolls kept in database for auditing (don't affect state).
🤖 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>
Core Implementation:
- Created roll_types.py with AbRoll, JumpRoll, FieldingRoll, D20Roll dataclasses
- Implemented DiceSystem singleton with cryptographically secure random generation
- Added Roll model to db_models.py with JSONB storage for roll history
- Implemented save_rolls_batch() and get_rolls_for_game() in database operations
Testing:
- 27 unit tests for roll type dataclasses (100% passing)
- 35 unit tests for dice system (34/35 passing, 1 timing issue)
- 16 integration tests for database persistence (uses production DiceSystem)
Features:
- Unique roll IDs using secrets.token_hex()
- League-specific logic (SBA d100 rare plays, PD error-based rare plays)
- Automatic derived value calculation (d6_two_total, jump_total, error_total)
- Full audit trail with context metadata
- Support for batch saving rolls per inning
Technical Details:
- Fixed dataclass inheritance with kw_only=True for Python 3.13
- Roll data stored as JSONB for flexible querying
- Indexed on game_id, roll_type, league_id, team_id for efficient retrieval
- Supports filtering by roll type, team, and timestamp ordering
Note: Integration tests have async connection pool issue when run together
(tests work individually, fixture cleanup needed in follow-up branch)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Updated Lineup model to support both leagues using the same pattern as RosterLink:
- Made card_id nullable (PD league)
- Added player_id nullable (SBA league)
- Added XOR CHECK constraint to ensure exactly one ID is populated
- Created league-specific methods: add_pd_lineup_card() and add_sba_lineup_player()
- Replaced generic create_lineup_entry() with league-specific methods
Database migration applied to convert existing schema.
Bonus fix: Resolved Pendulum DateTime + asyncpg timezone compatibility issue
by using .naive() on all DateTime defaults in Game, Play, and GameSession models.
Updated tests to use league-specific lineup methods.
Archived migration docs and script to .claude/archive/ for reference.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Added league-agnostic roster tracking with single-table design:
Database Changes:
- Modified RosterLink model with surrogate primary key (id)
- Added nullable card_id (PD) and player_id (SBA) columns
- Added CHECK constraint ensuring exactly one ID populated (XOR logic)
- Added unique constraints for (game_id, card_id) and (game_id, player_id)
- Imported CheckConstraint and UniqueConstraint from SQLAlchemy
New Files:
- app/models/roster_models.py: Pydantic models for type safety
- BaseRosterLinkData: Abstract base class
- PdRosterLinkData: PD league card-based rosters
- SbaRosterLinkData: SBA league player-based rosters
- RosterLinkCreate: Request validation model
- tests/unit/models/test_roster_models.py: 24 unit tests (all passing)
- Tests for PD/SBA roster link creation and validation
- Tests for RosterLinkCreate XOR validation
- Tests for polymorphic behavior
Database Operations:
- add_pd_roster_card(): Add PD card to game roster
- add_sba_roster_player(): Add SBA player to game roster
- get_pd_roster(): Get PD cards with optional team filter
- get_sba_roster(): Get SBA players with optional team filter
- remove_roster_entry(): Remove roster entry by ID
Tests:
- Added 12 integration tests for roster operations
- Fixed setup_database fixture scope (module → function)
Documentation:
- Updated backend/CLAUDE.md with RosterLink documentation
- Added usage examples and design rationale
- Updated Game model relationship description
Design Pattern:
Single table with application-layer type safety rather than SQLAlchemy
polymorphic inheritance. Simpler queries, database-enforced integrity,
and Pydantic type safety at application layer.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>