strat-gameplay-webapp/backend/terminal_client/CLAUDE.md
Cal Corum aabb90feb5 CLAUDE: Implement player models and optimize database queries
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>
2025-10-28 14:08:56 -05:00

762 lines
19 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Terminal Client - Game Engine Testing Tool
## Overview
Interactive REPL (Read-Eval-Print Loop) terminal UI for testing the game engine directly without WebSockets or frontend dependencies. Built with Python's `cmd` module, Click, and Rich for a polished CLI experience.
**Purpose**: Rapid iteration on game engine development without needing to build/maintain a web frontend during core logic development.
**Key Feature**: Persistent in-memory state - the game engine `state_manager` stays loaded throughout your REPL session, allowing you to test gameplay flow without losing state between commands.
## Usage Modes
### Interactive REPL (Recommended)
Start the interactive shell for persistent in-memory state:
```bash
python -m terminal_client
# Then type commands interactively:
⚾ > new_game
⚾ > defensive
⚾ > offensive
⚾ > resolve
⚾ > quick_play 10
⚾ > status
⚾ > quit
```
**Advantages**:
- ✅ Game state stays in memory across commands
- ✅ Fast iteration - no process startup overhead
- ✅ Natural workflow like `psql` or `redis-cli`
- ✅ Persistent event loop prevents database connection issues
### Standalone Commands (Alternative)
Run individual commands with persistent config file:
```bash
python -m terminal_client new-game
python -m terminal_client defensive --alignment normal
python -m terminal_client offensive --approach power
python -m terminal_client resolve
```
**Note**: Config file (`~/.terminal_client_config.json`) remembers current game between commands.
## Architecture
### Design Decisions
**Why Terminal UI?**
- ✅ Zero frontend overhead - test game logic immediately
- ✅ No WebSocket complexity - direct function calls to GameEngine
- ✅ Fast iteration - change code, test instantly
- ✅ Easy debugging - logs and state visible in same terminal
- ✅ CI/CD ready - can be scripted for automated testing
**Why Click over Typer?**
- ✅ Already installed (FastAPI dependency)
- ✅ Battle-tested and stable (v8.1.8)
- ✅ Python 3.13 compatible
- ✅ No version compatibility issues
**Why Rich?**
- ✅ Beautiful formatted output (colors, panels, tables)
- ✅ Clear game state visualization
- ✅ Better testing UX than plain print statements
**Why REPL (cmd module)?**
- ✅ Single persistent process - state manager stays in memory
- ✅ Persistent event loop - no database connection conflicts
- ✅ Command history and readline support
- ✅ Tab completion (future enhancement)
### Technology Stack
```python
click==8.1.8 # CLI framework (already installed via FastAPI)
rich==13.9.4 # Terminal formatting and colors
cmd (stdlib) # REPL framework (built into Python)
```
## Project Structure
```
terminal_client/
├── __init__.py # Package marker
├── __main__.py # Entry point - routes to REPL or CLI
├── repl.py # Interactive REPL with cmd.Cmd
├── main.py # Click CLI standalone commands
├── display.py # Rich formatting for game state
├── config.py # Persistent configuration file manager
└── CLAUDE.md # This file
```
## REPL Commands Reference
All commands in interactive REPL mode. Use underscores (e.g., `new_game` not `new-game`).
### new_game
Create a new game with lineups and start it (all-in-one).
```
⚾ > new_game [--league sba|pd] [--home-team N] [--away-team N]
Examples:
new_game
new_game --league pd
new_game --home-team 5 --away-team 3
```
Automatically:
1. Creates game in database
2. Generates 9-player lineups for both teams
3. Starts the game
4. Sets as current game
### defensive
Submit defensive decision.
```
⚾ > defensive [--alignment TYPE] [--infield DEPTH] [--outfield DEPTH] [--hold BASES]
Options:
--alignment normal, shifted_left, shifted_right, extreme_shift
--infield in, normal, back, double_play
--outfield in, normal, back
--hold Comma-separated bases (e.g., 1,3)
Examples:
defensive
defensive --alignment shifted_left
defensive --infield double_play --hold 1,3
```
### offensive
Submit offensive decision.
```
⚾ > offensive [--approach TYPE] [--steal BASES] [--hit-run] [--bunt]
Options:
--approach normal, contact, power, patient
--steal Comma-separated bases (e.g., 2,3)
--hit-run Enable hit-and-run
--bunt Attempt bunt
Examples:
offensive
offensive --approach power
offensive --steal 2 --hit-run
```
### resolve
Resolve the current play.
```
⚾ > resolve
```
Both defensive and offensive decisions must be submitted first.
Displays play result with dice roll, runner advancement, and updated state.
### status
Display current game state.
```
⚾ > status
```
Shows score, inning, outs, runners, and pending decisions.
### quick_play
Auto-play multiple plays with default decisions.
```
⚾ > quick_play [COUNT]
Examples:
quick_play # Play 1 play
quick_play 10 # Play 10 plays
quick_play 27 # Play ~3 innings
```
Perfect for rapidly advancing game state for testing.
### box_score
Display box score.
```
⚾ > box_score
```
### list_games
List all games in state manager (in memory).
```
⚾ > list_games
```
Shows active games with current game marked.
### use_game
Switch to a different game.
```
⚾ > use_game <game_id>
Example:
use_game a1b2c3d4-e5f6-7890-abcd-ef1234567890
```
### config
Show configuration.
```
⚾ > config
```
Displays config file path and current game.
### clear
Clear the screen.
```
⚾ > clear
```
### quit / exit
Exit the REPL.
```
⚾ > quit
⚾ > exit
```
Or press Ctrl+D.
---
## Standalone Commands Reference
For running individual commands outside REPL mode.
### new-game
```bash
python -m terminal_client new-game [OPTIONS]
Options:
--league TEXT League (sba or pd) [default: sba]
--game-id TEXT Game UUID (auto-generated if not provided)
--home-team INTEGER Home team ID [default: 1]
--away-team INTEGER Away team ID [default: 2]
Example:
python -m terminal_client start-game --league sba
python -m terminal_client start-game --game-id <uuid> --home-team 5 --away-team 3
```
### Defensive Decision
```bash
python -m terminal_client defensive [OPTIONS]
Options:
--game-id TEXT Game UUID (uses current if not provided)
--alignment TEXT Defensive alignment [default: normal]
Values: normal, shifted_left, shifted_right, extreme_shift
--infield TEXT Infield depth [default: normal]
Values: in, normal, back, double_play
--outfield TEXT Outfield depth [default: normal]
Values: in, normal, back
--hold TEXT Comma-separated bases to hold (e.g., 1,3)
Example:
python -m terminal_client defensive --alignment shifted_left
python -m terminal_client defensive --infield double_play --hold 1,3
```
### Offensive Decision
```bash
python -m terminal_client offensive [OPTIONS]
Options:
--game-id TEXT Game UUID (uses current if not provided)
--approach TEXT Batting approach [default: normal]
Values: normal, contact, power, patient
--steal TEXT Comma-separated bases to steal (e.g., 2,3)
--hit-run Hit-and-run play (flag)
--bunt Bunt attempt (flag)
Example:
python -m terminal_client offensive --approach power
python -m terminal_client offensive --steal 2 --hit-run
```
### Resolve Play
```bash
python -m terminal_client resolve [OPTIONS]
Options:
--game-id TEXT Game UUID (uses current if not provided)
Example:
python -m terminal_client resolve
```
Resolves the current play using submitted defensive and offensive decisions.
Displays full play result with dice rolls, runner advancement, and updated game state.
### Game Status
```bash
python -m terminal_client status [OPTIONS]
Options:
--game-id TEXT Game UUID (uses current if not provided)
Example:
python -m terminal_client status
```
### Box Score
```bash
python -m terminal_client box-score [OPTIONS]
Options:
--game-id TEXT Game UUID (uses current if not provided)
Example:
python -m terminal_client box-score
```
### Quick Play
```bash
python -m terminal_client quick-play [OPTIONS]
Options:
--count INTEGER Number of plays to execute [default: 1]
--game-id TEXT Game UUID (uses current if not provided)
Example:
python -m terminal_client quick-play --count 10
python -m terminal_client quick-play --count 27 # Full inning
```
**Use Case**: Rapidly advance game state for testing specific scenarios (e.g., test 9th inning logic).
Submits default decisions (normal alignment, normal approach) and auto-resolves plays.
### List Games
```bash
python -m terminal_client list-games
Example:
python -m terminal_client list-games
```
Shows all active games in the state manager.
### Use Game
```bash
python -m terminal_client use-game <game_id>
Example:
python -m terminal_client use-game a1b2c3d4-e5f6-7890-abcd-ef1234567890
```
Switch to a different game (sets as "current" game for subsequent commands).
## Usage Patterns
### Typical REPL Testing Workflow
```bash
# Start REPL
python -m terminal_client
# Create and play a game
⚾ > new_game
⚾ > defensive
⚾ > offensive
⚾ > resolve
⚾ > status
# Continue playing
⚾ > defensive --alignment shifted_left
⚾ > offensive --approach power
⚾ > resolve
# Or use quick-play to auto-advance
⚾ > quick_play 10
# Check final state
⚾ > status
⚾ > quit
```
**Advantages**: Game state stays in memory throughout the session!
### Testing Specific Scenarios
```bash
# Test defensive shifts
python -m terminal_client start-game
python -m terminal_client defensive --alignment shifted_left --infield double_play
python -m terminal_client offensive
python -m terminal_client resolve
# Test stealing
python -m terminal_client start-game
python -m terminal_client defensive
python -m terminal_client offensive --steal 2,3 # Double steal
python -m terminal_client resolve
# Test hit-and-run
python -m terminal_client defensive
python -m terminal_client offensive --hit-run
python -m terminal_client resolve
# Advance to late game quickly
python -m terminal_client start-game
python -m terminal_client quick-play --count 50 # ~6 innings
python -m terminal_client status
```
### Multiple Games
```bash
# Start game 1
python -m terminal_client start-game
# ... play some ...
# Start game 2
python -m terminal_client start-game
# ... play some ...
# List all games
python -m terminal_client list-games
# Switch back to game 1
python -m terminal_client use-game <game-1-uuid>
python -m terminal_client status
```
## Display Features
### Game State Panel
- **Game Info**: UUID, league, status
- **Score**: Away vs Home
- **Inning**: Current inning and half
- **Outs**: Current out count
- **Runners**: Bases occupied with lineup IDs
- **Current Players**: Batter, pitcher (by lineup ID)
- **Pending Decision**: Enhanced display showing what action is needed next (Added 2025-10-28)
- Shows "⚠️ WAITING FOR ACTION" header when play is pending
- Displays specific message: "The defense needs to submit their decision" or "Ready to resolve play"
- Shows exact command to run next: `defensive [OPTIONS]`, `offensive [OPTIONS]`, or `resolve`
- Color-coded for clarity (yellow warning + cyan command hints)
- **Last Play**: Result description
### Play Result Panel
- **Outcome**: Hit type (GB, FB, LD, etc.)
- **Result Description**: Human-readable play result
- **Dice Roll**: Actual d20 roll with context
- **Outs/Runs**: Changes from play
- **Runner Movement**: Base-by-base advancement
- **Updated Score**: Current score after play
### Color Coding
- ✅ Green: Success messages, runs scored
- ❌ Red: Error messages, outs recorded
- Blue: Info messages
- ⚠️ Yellow: Warning messages
- Cyan: Game state highlights
## Implementation Details
### Persistent Event Loop (REPL Mode)
The REPL uses a single persistent event loop for the entire session:
```python
def __init__(self):
# Create persistent event loop
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
def _run_async(self, coro):
# Reuse same loop for all commands
return self.loop.run_until_complete(coro)
def do_quit(self, arg):
# Clean up on exit
self.loop.close()
```
**Why**: Prevents database connection pool conflicts that occur when creating new event loops for each command.
### Game State Management
**REPL Mode**: Game state manager stays in memory throughout the session.
- In-memory state persists across all commands
- O(1) state lookups
- Perfect for testing gameplay flow
**Standalone Mode**: Uses persistent config file (`~/.terminal_client_config.json`).
- Stores current game UUID between command invocations
- Set automatically by `new-game` or `use-game`
- Used as default if `--game-id` not specified
### Async Command Pattern
All commands use the same async pattern:
```python
@cli.command()
@click.option('--game-id', default=None)
def some_command(game_id):
async def _some_command():
gid = UUID(game_id) if game_id else get_current_game()
# ... async operations ...
state = await game_engine.some_method(gid)
display.show_something(state)
asyncio.run(_some_command())
```
This allows direct async calls to GameEngine without WebSocket overhead.
### Error Handling
- **Click Abort**: Raises `click.Abort()` on errors (clean exit)
- **Rich Display**: All errors shown with colored formatting
- **Logger**: Exceptions logged with full traceback
- **User-Friendly**: Clear error messages (not stack traces)
## Testing with Terminal Client
### Unit Test Scenarios
```bash
# Test game startup validation
python -m terminal_client start-game
# Should fail: No lineups set
# Test invalid decisions
python -m terminal_client defensive --alignment invalid_value
# Should fail: ValidationError
# Test resolve without decisions
python -m terminal_client resolve
# Should fail: No decisions submitted
```
### Integration Test Scenarios
```bash
# Full game flow
python -m terminal_client start-game
# ... set lineups via database directly ...
python -m terminal_client start-game # Retry
python -m terminal_client quick-play --count 100
python -m terminal_client status
# Verify: Game status = "completed"
# State persistence
python -m terminal_client start-game
# Note the game UUID
python -m terminal_client quick-play --count 10
# Kill terminal
# Restart and use same UUID
python -m terminal_client use-game <uuid>
python -m terminal_client status
# Verify: State recovered from database
```
## Limitations
### Current Limitations (Phase 2)
1. **No Lineup Management**: Must set lineups via database directly
2. **No Player Data**: Shows lineup IDs only (no names/stats)
3. **Simple Box Score**: Only shows final scores (no detailed stats)
4. **No Substitutions**: Cannot make mid-game substitutions
5. **No AI Decisions**: All decisions manual (no AI opponent)
### Future Enhancements (Post-MVP)
- Interactive lineup builder
- Player name/stat display from league API
- Full box score with batting stats
- Substitution commands
- AI decision simulation
- Play-by-play export
- Replay mode for completed games
## Development Notes
### Adding New Commands
```python
# In main.py
@cli.command('new-command')
@click.option('--some-option', default='value', help='Description')
def new_command(some_option):
"""Command description for help text."""
async def _new_command():
# Implementation
pass
asyncio.run(_new_command())
```
### Adding New Display Functions
```python
# In display.py
def display_new_thing(data: SomeModel) -> None:
"""Display new thing with Rich formatting."""
panel = Panel(
Text("Content here"),
title="[bold]Title[/bold]",
border_style="color",
box=box.ROUNDED
)
console.print(panel)
```
### Logging
All commands use the module logger:
```python
logger = logging.getLogger(f'{__name__}.main')
logger.info("Game started")
logger.error("Failed to resolve play", exc_info=True)
```
Logs appear in both terminal and `backend/logs/app_YYYYMMDD.log`.
## Troubleshooting
### Command Not Found
```bash
# Wrong
python terminal_client/main.py # ❌
# Correct
python -m terminal_client start-game # ✅
```
### Import Errors
```bash
# Ensure you're in backend directory
cd backend
source venv/bin/activate
python -m terminal_client start-game
```
### Game Not Found
```bash
# Check active games
python -m terminal_client list-games
# Use specific game ID
python -m terminal_client status --game-id <uuid>
```
### Validation Errors
Check logs for detailed error messages:
```bash
tail -f logs/app_$(date +%Y%m%d).log
```
## Performance
### REPL Mode
- **Startup Time**: ~500ms (one-time on launch)
- **Command Execution**: <50ms (in-memory state lookups)
- **State Display**: <50ms (Rich rendering)
- **Quick Play (10 plays)**: ~4-5 seconds (includes 0.3s sleeps per play)
- **Memory**: ~10MB for active game state
### Standalone Mode
- **Per-Command Startup**: ~500ms (process + imports)
- **Command Execution**: <100ms (direct function calls)
- **Database Queries**: 50-100ms (config file + state recovery)
**REPL is significantly faster for iterative testing!**
## Configuration File
Location: `~/.terminal_client_config.json`
```json
{
"current_game_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
```
Automatically managed by:
- `new_game` command (sets current)
- `use_game` command (changes current)
- `config --clear` command (clears current)
## Related Documentation
- **Game Engine**: `../app/core/game_engine.py`
- **State Manager**: `../app/core/state_manager.py`
- **Game Models**: `../app/models/game_models.py`
- **Backend Guide**: `../CLAUDE.md`
## Recent Updates
### 2025-10-28: Enhanced Status Display
**Improvement**: Added user-friendly pending action guidance to status command.
**Changes**:
- Status display now shows prominent "⚠ WAITING FOR ACTION" section when play is pending
- Provides specific guidance based on game state:
- "The defense needs to submit their decision" Run `defensive [OPTIONS]`
- "The offense needs to submit their decision" Run `offensive [OPTIONS]`
- "Ready to resolve play - both teams have decided" Run `resolve`
- Command hints color-coded (yellow warnings + cyan command text + green command names)
- Makes testing workflow clearer by showing exactly what to do next
**Location**: `terminal_client/display.py:75-97`
**Usage Example**:
```bash
⚾ > status
╭─────────────────────────── Game State ───────────────────────────╮
│ ... │
│ │
│ ⚠️ WAITING FOR ACTION │
│ ──────────────────────────────────────── │
│ The defense needs to submit their decision. │
│ Run: defensive [OPTIONS]
│ │
╰───────────────────────────────────────────────────────────────────╯
```
---
**Created**: 2025-10-26
**Author**: Claude
**Purpose**: Testing tool for Phase 2 game engine development
**Status**: Complete - Interactive REPL with persistent state working perfectly!
**Last Updated**: 2025-10-28