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

19 KiB
Raw Blame History

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

Start the interactive shell for persistent in-memory state:

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:

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

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

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

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

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

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

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

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

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

python -m terminal_client list-games

Example:
  python -m terminal_client list-games

Shows all active games in the state manager.

Use Game

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

# 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

# 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

# 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:

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:

@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

# 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

# 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

# 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

# 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:

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

# Wrong
python terminal_client/main.py  # ❌

# Correct
python -m terminal_client start-game  # ✅

Import Errors

# Ensure you're in backend directory
cd backend
source venv/bin/activate
python -m terminal_client start-game

Game Not Found

# 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:

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

{
  "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)
  • 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:

⚾ > 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