"""Formula engine for refractor value computation (WP-09). Three pure functions that compute a numeric refractor value from career stats, plus helpers for formula dispatch and tier classification. Stats attributes expected by each formula: compute_batter_value: pa, hits, doubles, triples, hr (from BattingSeasonStats) compute_sp_value: outs, strikeouts (from PitchingSeasonStats) compute_rp_value: outs, strikeouts (from PitchingSeasonStats) """ from typing import Protocol class BatterStats(Protocol): pa: int hits: int doubles: int triples: int hr: int class PitcherStats(Protocol): outs: int strikeouts: int # --------------------------------------------------------------------------- # Core formula functions # --------------------------------------------------------------------------- def compute_batter_value(stats) -> float: """PA + (TB x 2) where TB = 1B + 2x2B + 3x3B + 4xHR.""" singles = stats.hits - stats.doubles - stats.triples - stats.hr tb = singles + 2 * stats.doubles + 3 * stats.triples + 4 * stats.hr return float(stats.pa + tb * 2) def _pitcher_value(stats) -> float: return stats.outs / 3 + stats.strikeouts def compute_sp_value(stats) -> float: """IP + K where IP = outs / 3.""" return _pitcher_value(stats) def compute_rp_value(stats) -> float: """IP + K (same formula as SP; thresholds differ).""" return _pitcher_value(stats) # --------------------------------------------------------------------------- # Dispatch and tier helpers # --------------------------------------------------------------------------- _FORMULA_DISPATCH = { "batter": compute_batter_value, "sp": compute_sp_value, "rp": compute_rp_value, } def compute_value_for_track(card_type: str, stats) -> float: """Dispatch to the correct formula function by card_type. Args: card_type: One of 'batter', 'sp', 'rp'. stats: Object with the attributes required by the formula. Raises: ValueError: If card_type is not recognised. """ fn = _FORMULA_DISPATCH.get(card_type) if fn is None: raise ValueError(f"Unknown card_type: {card_type!r}") return fn(stats) def tier_from_value(value: float, track) -> int: """Return the refractor tier (0-4) for a computed value against a track. Tier boundaries are inclusive on the lower end: T0: value < t1 T1: t1 <= value < t2 T2: t2 <= value < t3 T3: t3 <= value < t4 T4: value >= t4 Args: value: Computed formula value. track: Object (or dict-like) with t1_threshold..t4_threshold attributes/keys. """ # Support both attribute-style (Peewee model) and dict (seed fixture) if isinstance(track, dict): t1, t2, t3, t4 = ( track["t1_threshold"], track["t2_threshold"], track["t3_threshold"], track["t4_threshold"], ) else: t1, t2, t3, t4 = ( track.t1_threshold, track.t2_threshold, track.t3_threshold, track.t4_threshold, ) if value >= t4: return 4 if value >= t3: return 3 if value >= t2: return 2 if value >= t1: return 1 return 0