All checks were successful
Build Docker Image / build (push) Successful in 8m41s
The single-letter `k` field was ambiguous and too short for comfortable use. Rename to `so_pitcher` for clarity, and `so` to `so_batter` to distinguish batting strikeouts from pitching strikeouts in the same model. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
106 lines
3.0 KiB
Python
106 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
|
||
compute_sp_value: outs, so_pitcher (pitcher strikeouts, from PlayerSeasonStats)
|
||
compute_rp_value: outs, so_pitcher
|
||
"""
|
||
|
||
from typing import Protocol
|
||
|
||
|
||
class BatterStats(Protocol):
|
||
pa: int
|
||
hits: int
|
||
doubles: int
|
||
triples: int
|
||
hr: int
|
||
|
||
|
||
class PitcherStats(Protocol):
|
||
outs: int
|
||
so_pitcher: int
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Core formula functions
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def compute_batter_value(stats) -> float:
|
||
"""PA + (TB × 2) where TB = 1B + 2×2B + 3×3B + 4×HR."""
|
||
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 compute_sp_value(stats) -> float:
|
||
"""IP + K where IP = outs / 3. Uses stats.so_pitcher (pitcher strikeouts)."""
|
||
return stats.outs / 3 + stats.so_pitcher
|
||
|
||
|
||
def compute_rp_value(stats) -> float:
|
||
"""IP + K (same formula as SP; thresholds differ). Uses stats.so_pitcher."""
|
||
return stats.outs / 3 + stats.so_pitcher
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 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
|