""" Calculate statistical fields from PlayOutcome. Called when recording a play to populate stat fields for materialized view aggregation. """ import logging 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