""" Rule Validators - Validate game actions and state transitions. Ensures all game actions follow baseball rules and state is valid. Author: Claude Date: 2025-10-24 """ import logging from uuid import UUID from app.models.game_models import GameState, DefensiveDecision, OffensiveDecision logger = logging.getLogger(f'{__name__}.GameValidator') class ValidationError(Exception): """Raised when validation fails""" pass class GameValidator: """Validates game actions and state""" @staticmethod def validate_game_active(state: GameState) -> None: """Ensure game is in active state""" if state.status != "active": raise ValidationError(f"Game is not active (status: {state.status})") @staticmethod def validate_outs(outs: int) -> None: """Ensure outs are valid""" if outs < 0 or outs > 2: raise ValidationError(f"Invalid outs: {outs} (must be 0-2)") @staticmethod def validate_inning(inning: int, half: str) -> None: """Ensure inning is valid""" if inning < 1: raise ValidationError(f"Invalid inning: {inning}") if half not in ["top", "bottom"]: raise ValidationError(f"Invalid half: {half}") @staticmethod def validate_defensive_decision(decision: DefensiveDecision, state: GameState) -> None: """Validate defensive team decision""" valid_alignments = ["normal", "shifted_left", "shifted_right"] if decision.alignment not in valid_alignments: raise ValidationError(f"Invalid alignment: {decision.alignment}") valid_depths = ["in", "normal", "back", "double_play"] # TODO: update these to strat-specific values if decision.infield_depth not in valid_depths: raise ValidationError(f"Invalid infield depth: {decision.infield_depth}") # Validate hold runners - can't hold empty bases runner_bases = [r.on_base for r in state.runners] for base in decision.hold_runners: if base not in runner_bases: raise ValidationError(f"Can't hold base {base} - no runner present") logger.debug("Defensive decision validated") @staticmethod def validate_offensive_decision(decision: OffensiveDecision, state: GameState) -> None: """Validate offensive team decision""" valid_approaches = ["normal", "contact", "power", "patient"] # TODO: update these to strat-specific values if decision.approach not in valid_approaches: raise ValidationError(f"Invalid approach: {decision.approach}") # Validate steal attempts runner_bases = [r.on_base for r in state.runners] for base in decision.steal_attempts: # Must have runner on base-1 to steal base if (base - 1) not in runner_bases: raise ValidationError(f"Can't steal {base} - no runner on {base-1}") # TODO: add check that base in front of stealing runner is unoccupied # Can't bunt with 2 outs (simplified rule) if decision.bunt_attempt and state.outs == 2: raise ValidationError("Cannot bunt with 2 outs") logger.debug("Offensive decision validated") @staticmethod def validate_defensive_lineup_positions(lineup: list) -> None: """ Validate defensive lineup has exactly 1 active player per position. Args: lineup: List of LineupPlayerState objects Raises: ValidationError: If any position is missing or duplicated """ required_positions = ['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF'] # Count active players per position position_counts: dict[str, int] = {} for player in lineup: if player.is_active: pos = player.position position_counts[pos] = position_counts.get(pos, 0) + 1 # Check each required position has exactly 1 active player errors = [] for pos in required_positions: count = position_counts.get(pos, 0) if count == 0: errors.append(f"Missing active player at {pos}") elif count > 1: errors.append(f"Multiple active players at {pos} ({count} players)") if errors: raise ValidationError(f"Invalid defensive lineup: {'; '.join(errors)}") logger.debug("Defensive lineup positions validated") @staticmethod def can_continue_inning(state: GameState) -> bool: """Check if inning can continue""" return state.outs < 3 @staticmethod def is_game_over(state: GameState) -> bool: """Check if game is complete""" # Game over after 9 innings if score not tied if state.inning >= 9 and state.half == "bottom": if state.home_score != state.away_score: return True # Home team wins if ahead in bottom of 9th if state.home_score > state.away_score: return True # Also check if we're in extras and bottom team is ahead if state.inning > 9 and state.half == "bottom": if state.home_score > state.away_score: return True return False # Singleton instance game_validator = GameValidator()