189 lines
6.9 KiB
Python
189 lines
6.9 KiB
Python
import math
|
|
|
|
import pytest
|
|
|
|
from helpers.refractor_test_data import (
|
|
build_batter_plays,
|
|
build_decision_data,
|
|
build_game_data,
|
|
build_pitcher_plays,
|
|
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
|
|
|
|
|
|
class TestBuildGameData:
|
|
"""Test synthetic game record construction for refractor testing.
|
|
|
|
build_game_data creates a self-play game (team vs itself) with
|
|
game_type='test' and all score/ranking fields zeroed out. This
|
|
gives the minimum valid game payload to POST to the API.
|
|
"""
|
|
|
|
def test_basic_structure(self):
|
|
"""Core IDs, type flags, and boolean fields are correct."""
|
|
result = build_game_data(team_id=31, season=11)
|
|
assert result["away_team_id"] == 31
|
|
assert result["home_team_id"] == 31
|
|
assert result["season"] == 11
|
|
assert result["game_type"] == "test"
|
|
assert result["short_game"] is True
|
|
assert result["ranked"] is False
|
|
assert result["forfeit"] is False
|
|
|
|
def test_score_reflects_zero(self):
|
|
"""Scores start at zero — no actual game was simulated."""
|
|
result = build_game_data(team_id=31, season=11)
|
|
assert result["away_score"] == 0
|
|
assert result["home_score"] == 0
|
|
|
|
|
|
class TestBuildBatterPlays:
|
|
"""Test synthetic HR play construction for batter refractor testing.
|
|
|
|
Each play is a solo HR: PA=1, AB=1, H=1, HR=1, R=1, RBI=1.
|
|
Structural fields are filled with safe defaults (inning 1, bot half,
|
|
no runners on base, zero scores). play_num is sequential from 1.
|
|
"""
|
|
|
|
def test_correct_count(self):
|
|
"""num_plays controls how many play dicts are returned."""
|
|
plays = build_batter_plays(
|
|
game_id=1, batter_id=100, team_id=31, pitcher_id=200, num_plays=3
|
|
)
|
|
assert len(plays) == 3
|
|
|
|
def test_play_fields(self):
|
|
"""Each play has correct IDs and HR stat values."""
|
|
plays = build_batter_plays(
|
|
game_id=1, batter_id=100, team_id=31, pitcher_id=200, num_plays=1
|
|
)
|
|
play = plays[0]
|
|
assert play["game_id"] == 1
|
|
assert play["batter_id"] == 100
|
|
assert play["batter_team_id"] == 31
|
|
assert play["pitcher_id"] == 200
|
|
assert play["pitcher_team_id"] == 31
|
|
assert play["pa"] == 1
|
|
assert play["ab"] == 1
|
|
assert play["hit"] == 1
|
|
assert play["homerun"] == 1
|
|
assert play["run"] == 1
|
|
assert play["rbi"] == 1
|
|
|
|
def test_play_nums_sequential(self):
|
|
"""play_num increments from 1 for each play in the batch."""
|
|
plays = build_batter_plays(
|
|
game_id=1, batter_id=100, team_id=31, pitcher_id=200, num_plays=4
|
|
)
|
|
assert [p["play_num"] for p in plays] == [1, 2, 3, 4]
|
|
|
|
def test_required_structural_fields(self):
|
|
"""Structural fields are filled with safe defaults for API acceptance."""
|
|
plays = build_batter_plays(
|
|
game_id=1, batter_id=100, team_id=31, pitcher_id=200, num_plays=1
|
|
)
|
|
play = plays[0]
|
|
assert play["on_base_code"] == "000"
|
|
assert play["inning_half"] == "bot"
|
|
assert play["inning_num"] == 1
|
|
assert play["batting_order"] == 1
|
|
assert play["starting_outs"] == 0
|
|
assert play["away_score"] == 0
|
|
assert play["home_score"] == 0
|
|
|
|
|
|
class TestBuildPitcherPlays:
|
|
"""Test synthetic strikeout play construction for pitcher refractor testing.
|
|
|
|
Each play is a K: PA=1, AB=1, SO=1, outs=1, hit=0, homerun=0.
|
|
Structural fields mirror the batter play defaults.
|
|
"""
|
|
|
|
def test_correct_count(self):
|
|
"""num_plays controls how many play dicts are returned."""
|
|
plays = build_pitcher_plays(
|
|
game_id=1, pitcher_id=200, team_id=31, batter_id=100, num_plays=5
|
|
)
|
|
assert len(plays) == 5
|
|
|
|
def test_play_fields(self):
|
|
"""Each play has correct IDs and K stat values."""
|
|
plays = build_pitcher_plays(
|
|
game_id=1, pitcher_id=200, team_id=31, batter_id=100, num_plays=1
|
|
)
|
|
play = plays[0]
|
|
assert play["game_id"] == 1
|
|
assert play["pitcher_id"] == 200
|
|
assert play["pitcher_team_id"] == 31
|
|
assert play["batter_id"] == 100
|
|
assert play["batter_team_id"] == 31
|
|
assert play["pa"] == 1
|
|
assert play["ab"] == 1
|
|
assert play["so"] == 1
|
|
assert play["outs"] == 1
|
|
assert play["hit"] == 0
|
|
assert play["homerun"] == 0
|
|
|
|
|
|
class TestBuildDecisionData:
|
|
"""Test synthetic pitcher decision construction for refractor testing.
|
|
|
|
Returns a decisions payload with a single no-decision start entry.
|
|
All win/loss/hold/save flags default to 0; is_start is True.
|
|
"""
|
|
|
|
def test_basic_structure(self):
|
|
"""Decisions payload has correct IDs, season, and default flags."""
|
|
result = build_decision_data(game_id=1, pitcher_id=200, team_id=31, season=11)
|
|
assert result["decisions"][0]["game_id"] == 1
|
|
assert result["decisions"][0]["pitcher_id"] == 200
|
|
assert result["decisions"][0]["pitcher_team_id"] == 31
|
|
assert result["decisions"][0]["season"] == 11
|
|
assert result["decisions"][0]["is_start"] is True
|
|
assert result["decisions"][0]["win"] == 0
|
|
assert result["decisions"][0]["loss"] == 0
|