feat: add refractor test stat calculation helpers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9922f3bb3e
commit
0aeed0c76f
43
helpers/refractor_test_data.py
Normal file
43
helpers/refractor_test_data.py
Normal file
@ -0,0 +1,43 @@
|
||||
"""Pure helper functions for the /dev refractor-test command.
|
||||
|
||||
Builds synthetic game data to push a card over its next refractor
|
||||
tier threshold with the minimum number of plays.
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
# Batter: value = PA + (TB * 2). A HR play: PA=1, TB=4 → value = 1 + 8 = 9
|
||||
BATTER_VALUE_PER_PLAY = 9
|
||||
|
||||
# Pitcher: value = IP + K. A K play: outs=1 (IP=1/3), K=1 → value = 1/3 + 1 = 4/3
|
||||
PITCHER_VALUE_PER_PLAY = 4 / 3
|
||||
|
||||
|
||||
def calculate_plays_needed(gap: int, card_type: str) -> dict:
|
||||
"""Calculate the number of synthetic plays needed to close a refractor gap.
|
||||
|
||||
Args:
|
||||
gap: Points needed to reach the next tier threshold.
|
||||
A gap of 0 means the card is exactly at threshold — we still
|
||||
need 1 play to push past it.
|
||||
card_type: One of "batter", "sp", "rp".
|
||||
|
||||
Returns:
|
||||
dict with keys:
|
||||
num_plays: int — number of plays to create
|
||||
total_value: float — total refractor value those plays will add
|
||||
value_per_play: float — value each play contributes
|
||||
"""
|
||||
if card_type == "batter":
|
||||
value_per_play = BATTER_VALUE_PER_PLAY
|
||||
else:
|
||||
value_per_play = PITCHER_VALUE_PER_PLAY
|
||||
|
||||
num_plays = max(1, math.ceil(gap / value_per_play))
|
||||
total_value = num_plays * value_per_play
|
||||
|
||||
return {
|
||||
"num_plays": num_plays,
|
||||
"total_value": total_value,
|
||||
"value_per_play": value_per_play,
|
||||
}
|
||||
49
tests/test_refractor_test_data.py
Normal file
49
tests/test_refractor_test_data.py
Normal file
@ -0,0 +1,49 @@
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
||||
from helpers.refractor_test_data import calculate_plays_needed
|
||||
|
||||
|
||||
class TestCalculatePlaysNeeded:
|
||||
"""Test the pure function that computes how many synthetic plays
|
||||
are needed to push a card's refractor value over the next tier
|
||||
threshold. The formulas are:
|
||||
- batter: each HR play = 9 value (1 PA + 4 TB * 2)
|
||||
- sp/rp: each K play = 4/3 value (1/3 IP + 1 K)
|
||||
"""
|
||||
|
||||
def test_batter_exact_threshold(self):
|
||||
"""When the gap is exactly divisible by 9, no extra plays needed."""
|
||||
result = calculate_plays_needed(gap=27, card_type="batter")
|
||||
assert result["num_plays"] == 3
|
||||
assert result["total_value"] == 27
|
||||
assert result["value_per_play"] == 9
|
||||
|
||||
def test_batter_rounds_up(self):
|
||||
"""When gap isn't divisible by 9, round up to overshoot."""
|
||||
result = calculate_plays_needed(gap=10, card_type="batter")
|
||||
assert result["num_plays"] == 2 # ceil(10/9) = 2
|
||||
assert result["total_value"] == 18
|
||||
|
||||
def test_batter_gap_of_one(self):
|
||||
"""Even a gap of 1 requires one play."""
|
||||
result = calculate_plays_needed(gap=1, card_type="batter")
|
||||
assert result["num_plays"] == 1
|
||||
assert result["total_value"] == 9
|
||||
|
||||
def test_sp_exact_threshold(self):
|
||||
"""SP: each K play = 4/3 value."""
|
||||
result = calculate_plays_needed(gap=4, card_type="sp")
|
||||
assert result["num_plays"] == 3 # ceil(4 / (4/3)) = 3
|
||||
assert result["value_per_play"] == pytest.approx(4 / 3)
|
||||
|
||||
def test_rp_same_as_sp(self):
|
||||
"""RP uses the same formula as SP."""
|
||||
result = calculate_plays_needed(gap=4, card_type="rp")
|
||||
assert result["num_plays"] == 3
|
||||
|
||||
def test_zero_gap_returns_one_play(self):
|
||||
"""If already at threshold, still need 1 play to push over."""
|
||||
result = calculate_plays_needed(gap=0, card_type="batter")
|
||||
assert result["num_plays"] == 1
|
||||
Loading…
Reference in New Issue
Block a user