All checks were successful
Build Docker Image / build (push) Successful in 8m46s
Extract shared pitcher value computation into _pitcher_value() helper. Consolidate duplicated column lists and index helper in season stats tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
110 lines
3.0 KiB
Python
110 lines
3.0 KiB
Python
"""Formula engine for evolution value computation (WP-09).
|
|
|
|
Three pure functions that compute a numeric evolution 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 evolution 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, t2, t3, t4 attributes/keys.
|
|
"""
|
|
# Support both attribute-style (Peewee model) and dict (seed fixture)
|
|
if isinstance(track, dict):
|
|
t1, t2, t3, t4 = track["t1"], track["t2"], track["t3"], track["t4"]
|
|
else:
|
|
t1, t2, t3, t4 = track.t1, track.t2, track.t3, track.t4
|
|
|
|
if value >= t4:
|
|
return 4
|
|
if value >= t3:
|
|
return 3
|
|
if value >= t2:
|
|
return 2
|
|
if value >= t1:
|
|
return 1
|
|
return 0
|