strat-gameplay-webapp/backend/terminal_client/CLAUDE.md
Cal Corum 440adf2c26 CLAUDE: Update REPL for new GameState and standardize UV commands
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>
2025-11-04 09:59:13 -06:00

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

uv run python -m terminal_client
# Or: source .venv/bin/activate && 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:

uv run python -m terminal_client new-game
uv run python -m terminal_client defensive --alignment normal
uv run python -m terminal_client offensive --approach power
uv run 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

uv run 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:
  uv run python -m terminal_client start-game --league sba
  uv run python -m terminal_client start-game --game-id <uuid> --home-team 5 --away-team 3

Defensive Decision

uv run 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:
  uv run python -m terminal_client defensive --alignment shifted_left
  uv run python -m terminal_client defensive --infield double_play --hold 1,3

Offensive Decision

uv run 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:
  uv run python -m terminal_client offensive --approach power
  uv run python -m terminal_client offensive --steal 2 --hit-run

Resolve Play

uv run python -m terminal_client resolve [OPTIONS]

Options:
  --game-id TEXT  Game UUID (uses current if not provided)

Example:
  uv run 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

uv run python -m terminal_client status [OPTIONS]

Options:
  --game-id TEXT  Game UUID (uses current if not provided)

Example:
  uv run python -m terminal_client status

Box Score

uv run python -m terminal_client box-score [OPTIONS]

Options:
  --game-id TEXT  Game UUID (uses current if not provided)

Example:
  uv run python -m terminal_client box-score

Quick Play

uv run 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:
  uv run python -m terminal_client quick-play --count 10
  uv run 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

uv run python -m terminal_client list-games

Example:
  uv run python -m terminal_client list-games

Shows all active games in the state manager.

Use Game

uv run python -m terminal_client use-game <game_id>

Example:
  uv run 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
uv run python -m terminal_client
# Or: source .venv/bin/activate && 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
uv run python -m terminal_client start-game
uv run python -m terminal_client defensive --alignment shifted_left --infield double_play
uv run python -m terminal_client offensive
uv run python -m terminal_client resolve

# Test stealing
uv run python -m terminal_client start-game
uv run python -m terminal_client defensive
uv run python -m terminal_client offensive --steal 2,3  # Double steal
uv run python -m terminal_client resolve

# Test hit-and-run
uv run python -m terminal_client defensive
uv run python -m terminal_client offensive --hit-run
uv run python -m terminal_client resolve

# Advance to late game quickly
uv run python -m terminal_client start-game
uv run python -m terminal_client quick-play --count 50  # ~6 innings
uv run python -m terminal_client status

Multiple Games

# Start game 1
uv run python -m terminal_client start-game
# ... play some ...

# Start game 2
uv run python -m terminal_client start-game
# ... play some ...

# List all games
uv run python -m terminal_client list-games

# Switch back to game 1
uv run python -m terminal_client use-game <game-1-uuid>
uv run 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
uv run python -m terminal_client start-game
# Should fail: No lineups set

# Test invalid decisions
uv run python -m terminal_client defensive --alignment invalid_value
# Should fail: ValidationError

# Test resolve without decisions
uv run python -m terminal_client resolve
# Should fail: No decisions submitted

Integration Test Scenarios

# Full game flow
uv run python -m terminal_client start-game
# ... set lineups via database directly ...
uv run python -m terminal_client start-game  # Retry
uv run python -m terminal_client quick-play --count 100
uv run python -m terminal_client status
# Verify: Game status = "completed"

# State persistence
uv run python -m terminal_client start-game
# Note the game UUID
uv run python -m terminal_client quick-play --count 10
# Kill terminal
# Restart and use same UUID
uv run python -m terminal_client use-game <uuid>
uv run 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
uv run python -m terminal_client start-game  # ✅

Import Errors

# Ensure you're in backend directory
cd backend

# Recommended: Use UV (auto-activates venv)
uv run python -m terminal_client start-game

# Alternative: Manually activate venv
source .venv/bin/activate
python -m terminal_client start-game

Game Not Found

# Check active games
uv run python -m terminal_client list-games

# Use specific game ID
uv run 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