diff --git a/helpers/refractor_test_data.py b/helpers/refractor_test_data.py new file mode 100644 index 0000000..eef30ec --- /dev/null +++ b/helpers/refractor_test_data.py @@ -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, + } diff --git a/tests/test_refractor_test_data.py b/tests/test_refractor_test_data.py new file mode 100644 index 0000000..e146e6f --- /dev/null +++ b/tests/test_refractor_test_data.py @@ -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