diff --git a/backend/terminal_client/commands.py b/backend/terminal_client/commands.py index 75d9066..dc613ac 100644 --- a/backend/terminal_client/commands.py +++ b/backend/terminal_client/commands.py @@ -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() diff --git a/backend/terminal_client/repl.py b/backend/terminal_client/repl.py index 4dca8dd..7d5b12c 100644 --- a/backend/terminal_client/repl.py +++ b/backend/terminal_client/repl.py @@ -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 [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 [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 [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 [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.