paper-dynasty-database/app/services/formula_engine.py
Cal Corum 4ed62dea2c
All checks were successful
Build Docker Image / build (push) Successful in 8m41s
refactor: rename PlayerSeasonStats so to so_batter and k to so_pitcher
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>
2026-03-17 09:31:52 -05:00

106 lines
3.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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 (04) 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