CLAUDE: Add terminal client commands for manual outcome testing

New Commands:
- manual_outcome <outcome> [location] - Validates ManualOutcomeSubmission
  - Tests outcome and location validation
  - Shows which outcomes require hit location
  - Displays clear validation errors

- test_location <outcome> [handedness] [count] - Tests hit location distribution
  - Generates sample hit locations for an outcome
  - Shows distribution table with percentages
  - Validates pull rates (45% pull, 35% center, 20% opposite)
  - Supports both LHB and RHB

Implementation:
- Added validate_manual_outcome() to GameCommands class
- Added test_hit_location() to GameCommands class
- Added do_manual_outcome() to REPL
- Added do_test_location() to REPL
- Uses ManualOutcomeSubmission model from Task 3
- Uses calculate_hit_location() helper from Task 3

Testing:
- Tested manual_outcome with valid outcomes (groundball_c SS, strikeout)
- Tested manual_outcome with invalid outcome (proper error display)
- Tested test_location with groundball_c for RHB (shows distribution)
- All validation and display working correctly

Note: Full play resolution integration deferred to Week 7 Task 6 (WebSocket handlers).
      These commands validate and test the new models but don't resolve plays yet.

Files Modified:
- terminal_client/commands.py (+117 lines)
- terminal_client/repl.py (+65 lines)
This commit is contained in:
Cal Corum 2025-10-30 15:25:30 -05:00
parent 9245b4e008
commit b40465ca8a
2 changed files with 182 additions and 0 deletions

View File

@ -403,6 +403,123 @@ class GameCommands:
display.print_error(f"Failed to get box score: {e}")
return False
def validate_manual_outcome(self, outcome: str, location: Optional[str] = None) -> bool:
"""
Validate a manual outcome submission.
Args:
outcome: PlayOutcome enum value (e.g., 'groundball_c')
location: Optional hit location (e.g., 'SS')
Returns:
True if valid, False otherwise
"""
from app.models.game_models import ManualOutcomeSubmission
from pydantic import ValidationError
try:
# Try to create ManualOutcomeSubmission
submission = ManualOutcomeSubmission(
outcome=outcome,
hit_location=location
)
# Show success
display.print_success(f"✅ Valid manual outcome submission")
display.console.print(f" [cyan]Outcome:[/cyan] [green]{submission.outcome}[/green]")
if submission.hit_location:
display.console.print(f" [cyan]Location:[/cyan] [green]{submission.hit_location}[/green]")
else:
display.console.print(f" [cyan]Location:[/cyan] [dim]None (not required for this outcome)[/dim]")
# Check if location is required
outcome_enum = PlayOutcome(outcome)
if outcome_enum.requires_hit_location():
if not location:
display.print_warning("⚠️ Note: This outcome typically requires a hit location")
display.console.print(" [dim]Groundballs and flyouts need location for runner advancement[/dim]")
return True
except ValidationError as e:
display.print_error("❌ Invalid manual outcome submission")
for error in e.errors():
field = error['loc'][0] if error['loc'] else 'unknown'
message = error['msg']
display.console.print(f" [red]•[/red] [yellow]{field}:[/yellow] {message}")
return False
except Exception as e:
display.print_error(f"Validation error: {e}")
return False
def test_hit_location(self, outcome: str, handedness: str = 'R', count: int = 10) -> None:
"""
Test hit location calculation for a given outcome and handedness.
Args:
outcome: PlayOutcome enum value (e.g., 'groundball_c')
handedness: Batter handedness ('L' or 'R')
count: Number of samples to generate
Displays:
Distribution of hit locations
"""
from app.config.result_charts import calculate_hit_location, PlayOutcome
from collections import Counter
from rich.table import Table
try:
outcome_enum = PlayOutcome(outcome)
except ValueError:
display.print_error(f"Invalid outcome: {outcome}")
display.console.print(" [dim]Use 'list_outcomes' to see valid outcomes[/dim]")
return
# Generate samples
locations = []
for _ in range(count):
location = calculate_hit_location(outcome_enum, handedness)
if location:
locations.append(location)
if not locations:
display.print_info(f"Outcome '{outcome}' does not require hit location")
display.console.print(" [dim]Location only tracked for groundballs and flyouts[/dim]")
return
# Count distribution
counter = Counter(locations)
total = len(locations)
# Display results
display.print_success(f"Hit Location Distribution for {outcome} ({handedness}HB)")
table = Table(show_header=True, header_style="bold cyan")
table.add_column("Location", style="yellow", width=15)
table.add_column("Count", style="green", width=10, justify="right")
table.add_column("Percentage", style="cyan", width=15, justify="right")
table.add_column("Visual", style="white", width=30)
for location in sorted(counter.keys()):
count_val = counter[location]
pct = (count_val / total) * 100
bar = "" * int(pct / 3) # Scale bar to fit
table.add_row(
location,
str(count_val),
f"{pct:.1f}%",
bar
)
display.console.print(table)
# Show pull rates info
display.console.print(f"\n[dim]Pull rates: 45% pull, 35% center, 20% opposite[/dim]")
if handedness == 'R':
display.console.print(f"[dim]RHB pulls left (3B, SS, LF)[/dim]")
else:
display.console.print(f"[dim]LHB pulls right (1B, 2B, RF)[/dim]")
# Singleton instance
game_commands = GameCommands()

View File

@ -417,6 +417,71 @@ Press Ctrl+D or type 'quit' to exit.
self._run_async(_box_score())
def do_manual_outcome(self, arg):
"""
Validate a manual outcome submission (for testing manual mode).
Usage: manual_outcome <outcome> [location]
Arguments:
outcome PlayOutcome enum value (e.g., groundball_c, single_1)
location Hit location (e.g., SS, 1B, LF) - optional
Examples:
manual_outcome strikeout
manual_outcome groundball_c SS
manual_outcome flyout_b LF
manual_outcome single_1
Note: This validates the outcome but doesn't resolve the play yet.
Full integration with play resolution coming in Week 7 Task 6.
"""
parts = arg.split()
if not parts:
display.print_error("Usage: manual_outcome <outcome> [location]")
display.console.print("[dim]Example: manual_outcome groundball_c SS[/dim]")
return
outcome = parts[0]
location = parts[1] if len(parts) > 1 else None
game_commands.validate_manual_outcome(outcome, location)
def do_test_location(self, arg):
"""
Test hit location distribution for an outcome.
Usage: test_location <outcome> [handedness] [count]
Arguments:
outcome PlayOutcome enum value (e.g., groundball_c)
handedness 'L' or 'R' (default: R)
count Number of samples (default: 100)
Examples:
test_location groundball_c
test_location groundball_c L
test_location flyout_b R 200
Shows distribution of hit locations based on pull rates.
"""
parts = arg.split()
if not parts:
display.print_error("Usage: test_location <outcome> [handedness] [count]")
display.console.print("[dim]Example: test_location groundball_c R 100[/dim]")
return
outcome = parts[0]
handedness = parts[1] if len(parts) > 1 else 'R'
count = int(parts[2]) if len(parts) > 2 else 100
# Validate handedness
if handedness not in ['L', 'R']:
display.print_error("Handedness must be 'L' or 'R'")
return
game_commands.test_hit_location(outcome, handedness, count)
def do_list_games(self, arg):
"""
List all games in state manager.