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>
19 KiB
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:
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
psqlorredis-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:
- Creates game in database
- Generates 9-player lineups for both teams
- Starts the game
- 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], orresolve - 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-gameoruse-game - Used as default if
--game-idnot 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)
- No Lineup Management: Must set lineups via database directly
- No Player Data: Shows lineup IDs only (no names/stats)
- Simple Box Score: Only shows final scores (no detailed stats)
- No Substitutions: Cannot make mid-game substitutions
- 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_gamecommand (sets current)use_gamecommand (changes current)config --clearcommand (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
- "The defense needs to submit their decision" → Run
- 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