""" Calculate statistical fields from PlayOutcome. Called when recording a play to populate stat fields for materialized view aggregation. """ import logging from typing import Dict from app.config.result_charts import PlayOutcome from app.core.play_resolver import PlayResult from app.models.game_models import GameState logger = logging.getLogger(f'{__name__}.PlayStatCalculator') class PlayStatCalculator: """ Converts play outcome to statistical fields for database. Stats are written to plays table and aggregated via materialized views. Following legacy API pattern from major-domo /plays/batting and /plays/pitching. """ @staticmethod def calculate_stats( outcome: PlayOutcome, result: PlayResult, state_before: GameState, state_after: GameState ) -> Dict[str, int]: """ Calculate all stat fields for a play. Args: outcome: The PlayOutcome enum value result: PlayResult with detailed outcome info state_before: GameState before play resolution state_after: GameState after play resolution Returns: dict with stat fields (pa, ab, hit, run, etc.) """ stats: Dict[str, int] = { 'pa': 0, 'ab': 0, 'hit': 0, 'run': 0, 'rbi': 0, 'double': 0, 'triple': 0, 'homerun': 0, 'bb': 0, 'so': 0, 'hbp': 0, 'sac': 0, 'ibb': 0, 'sb': 0, 'cs': 0, 'gidp': 0, 'error': 0, 'wild_pitch': 0, 'passed_ball': 0, 'balk': 0, 'pick_off': 0, 'outs_recorded': 0 } # Plate appearance (everything except non-PA events) if outcome not in [ PlayOutcome.STOLEN_BASE, PlayOutcome.CAUGHT_STEALING, PlayOutcome.WILD_PITCH, PlayOutcome.PASSED_BALL, PlayOutcome.BALK, PlayOutcome.PICK_OFF ]: stats['pa'] = 1 # At bat (PA minus walks, HBP, sac) if stats['pa'] == 1 and outcome not in [ PlayOutcome.WALK, PlayOutcome.HIT_BY_PITCH, PlayOutcome.INTENTIONAL_WALK ]: stats['ab'] = 1 # Hits - check if outcome is a hit if outcome.is_hit(): stats['hit'] = 1 # Determine hit type if outcome in [ PlayOutcome.DOUBLE_2, PlayOutcome.DOUBLE_3, PlayOutcome.DOUBLE_UNCAPPED ]: stats['double'] = 1 elif outcome == PlayOutcome.TRIPLE: stats['triple'] = 1 elif outcome == PlayOutcome.HOMERUN: stats['homerun'] = 1 # Other batting outcomes if outcome == PlayOutcome.WALK: stats['bb'] = 1 elif outcome == PlayOutcome.INTENTIONAL_WALK: stats['ibb'] = 1 stats['bb'] = 1 # IBB counts as BB too elif outcome == PlayOutcome.STRIKEOUT: stats['so'] = 1 elif outcome == PlayOutcome.HIT_BY_PITCH: stats['hbp'] = 1 elif outcome == PlayOutcome.GROUNDBALL_A: # GROUNDBALL_A is double play if possible stats['gidp'] = 1 # Baserunning events (non-PA) if outcome == PlayOutcome.STOLEN_BASE: stats['sb'] = 1 elif outcome == PlayOutcome.CAUGHT_STEALING: stats['cs'] = 1 # Pitching events (non-PA) if outcome == PlayOutcome.WILD_PITCH: stats['wild_pitch'] = 1 elif outcome == PlayOutcome.PASSED_BALL: stats['passed_ball'] = 1 elif outcome == PlayOutcome.BALK: stats['balk'] = 1 elif outcome == PlayOutcome.PICK_OFF: stats['pick_off'] = 1 # Calculate runs scored from state change # Check which team is batting to determine which score changed if state_before.half == 'top': stats['run'] = state_after.away_score - state_before.away_score else: stats['run'] = state_after.home_score - state_before.home_score # RBI logic - runs scored minus runs on errors # If play had an error, don't credit RBI if hasattr(result, 'error_occurred') and result.error_occurred: stats['rbi'] = 0 else: stats['rbi'] = stats['run'] # Outs recorded stats['outs_recorded'] = state_after.outs - state_before.outs # Error tracking (from X-Check plays) if hasattr(result, 'error_occurred') and result.error_occurred: stats['error'] = 1 return stats