From 2067a02a230dc29c56416e48ee0123bc4085380d Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Thu, 9 Apr 2026 07:12:15 -0500 Subject: [PATCH] feat: add play/game/decision data builders for refractor test Co-Authored-By: Claude Opus 4.6 (1M context) --- helpers/refractor_test_data.py | 170 ++++++++++++++++++++++++++++++ tests/test_refractor_test_data.py | 141 ++++++++++++++++++++++++- 2 files changed, 310 insertions(+), 1 deletion(-) diff --git a/helpers/refractor_test_data.py b/helpers/refractor_test_data.py index eef30ec..679e3a2 100644 --- a/helpers/refractor_test_data.py +++ b/helpers/refractor_test_data.py @@ -41,3 +41,173 @@ def calculate_plays_needed(gap: int, card_type: str) -> dict: "total_value": total_value, "value_per_play": value_per_play, } + + +def build_game_data(team_id: int, season: int) -> dict: + """Build a minimal game record for refractor testing. + + Creates a self-play game (team vs itself) with game_type='test'. + All score and ranking fields are zeroed; short_game=True avoids + full simulation overhead when this record is posted to the API. + """ + return { + "season": season, + "game_type": "test", + "away_team_id": team_id, + "home_team_id": team_id, + "week": 1, + "away_score": 0, + "home_score": 0, + "away_team_value": 0, + "home_team_value": 0, + "away_team_ranking": 0, + "home_team_ranking": 0, + "ranked": False, + "short_game": True, + "forfeit": False, + } + + +def build_batter_plays( + game_id: int, + batter_id: int, + team_id: int, + pitcher_id: int, + num_plays: int, +) -> list[dict]: + """Build a list of synthetic solo-HR batter plays for refractor testing. + + Each play is a solo home run (PA=1, AB=1, H=1, HR=1, R=1, RBI=1). + Structural fields use safe defaults so the batch is accepted by the + plays API endpoint without requiring real game context. play_num is + sequential starting at 1. + + Args: + game_id: ID of the game these plays belong to. + batter_id: Card/player ID of the batter receiving credit. + team_id: Team ID used for both batter_team_id and pitcher_team_id + (self-play game). + pitcher_id: Card/player ID of the opposing pitcher. + num_plays: Number of HR plays to generate. + + Returns: + List of play dicts, one per home run. + """ + plays = [] + for i in range(num_plays): + plays.append( + { + "game_id": game_id, + "play_num": i + 1, + "batter_id": batter_id, + "batter_team_id": team_id, + "pitcher_id": pitcher_id, + "pitcher_team_id": team_id, + "pa": 1, + "ab": 1, + "hit": 1, + "homerun": 1, + "run": 1, + "rbi": 1, + "on_base_code": "000", + "inning_half": "bot", + "inning_num": 1, + "batting_order": 1, + "starting_outs": 0, + "away_score": 0, + "home_score": 0, + } + ) + return plays + + +def build_pitcher_plays( + game_id: int, + pitcher_id: int, + team_id: int, + batter_id: int, + num_plays: int, +) -> list[dict]: + """Build a list of synthetic strikeout pitcher plays for refractor testing. + + Each play is a strikeout (PA=1, AB=1, SO=1, outs=1, hit=0, homerun=0). + Structural fields use the same safe defaults as build_batter_plays. + play_num is sequential starting at 1. + + Args: + game_id: ID of the game these plays belong to. + pitcher_id: Card/player ID of the pitcher receiving credit. + team_id: Team ID used for both pitcher_team_id and batter_team_id + (self-play game). + batter_id: Card/player ID of the opposing batter. + num_plays: Number of strikeout plays to generate. + + Returns: + List of play dicts, one per strikeout. + """ + plays = [] + for i in range(num_plays): + plays.append( + { + "game_id": game_id, + "play_num": i + 1, + "pitcher_id": pitcher_id, + "pitcher_team_id": team_id, + "batter_id": batter_id, + "batter_team_id": team_id, + "pa": 1, + "ab": 1, + "so": 1, + "outs": 1, + "hit": 0, + "homerun": 0, + "on_base_code": "000", + "inning_half": "bot", + "inning_num": 1, + "batting_order": 1, + "starting_outs": 0, + "away_score": 0, + "home_score": 0, + } + ) + return plays + + +def build_decision_data( + game_id: int, + pitcher_id: int, + team_id: int, + season: int, +) -> dict: + """Build a minimal pitcher decision payload for refractor testing. + + Returns a decisions wrapper dict containing a single no-decision start + entry. All win/loss/hold/save flags default to 0; is_start is True + so the pitcher accrues IP-based refractor value from the associated plays. + + Args: + game_id: ID of the game the decision belongs to. + pitcher_id: Card/player ID of the pitcher. + team_id: Team ID for pitcher_team_id. + season: Season number for the decision record. + + Returns: + Dict with key "decisions" containing a list with one decision dict. + """ + return { + "decisions": [ + { + "game_id": game_id, + "season": season, + "week": 1, + "pitcher_id": pitcher_id, + "pitcher_team_id": team_id, + "win": 0, + "loss": 0, + "hold": 0, + "is_save": 0, + "is_start": True, + "b_save": 0, + } + ] + } diff --git a/tests/test_refractor_test_data.py b/tests/test_refractor_test_data.py index e146e6f..1fd7547 100644 --- a/tests/test_refractor_test_data.py +++ b/tests/test_refractor_test_data.py @@ -2,7 +2,13 @@ import math import pytest -from helpers.refractor_test_data import calculate_plays_needed +from helpers.refractor_test_data import ( + build_batter_plays, + build_decision_data, + build_game_data, + build_pitcher_plays, + calculate_plays_needed, +) class TestCalculatePlaysNeeded: @@ -47,3 +53,136 @@ class TestCalculatePlaysNeeded: """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