paper-dynasty-discord/tests/in_game/test_pitcher_decisions.py
Cal Corum ee80cd72ae fix: apply Black formatting and resolve ruff lint violations
Run Black formatter across 83 files and fix 1514 ruff violations:
- E722: bare except → typed exceptions (17 fixes)
- E711/E712/E721: comparison style fixes with noqa for SQLAlchemy (44 fixes)
- F841: unused variable assignments (70 fixes)
- F541/F401: f-string and import cleanup (1383 auto-fixes)

Remaining 925 errors are all F403/F405 (star imports) — structural,
requires converting to explicit imports in a separate effort.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 11:37:46 -05:00

428 lines
15 KiB
Python

"""Tests for pitcher decision logic in get_db_ready_decisions."""
from sqlmodel import Session
from in_game.gameplay_queries import get_db_ready_decisions
from in_game.gameplay_models import Game, Lineup, Play
class TestPitcherDecisions:
"""Test pitcher decision scenarios (Win, Loss, Save, Hold, Blown Save)."""
def get_factory_game_and_lineups(
self, session: Session
) -> tuple[Game, dict[str, Lineup]]:
"""Get existing game and lineups from factory data."""
# Use existing game from factory: Game 3 (teams 69 vs 420, active=True)
game = session.get(Game, 3)
# Factory creates lineups with these IDs:
# Team 69 (away): lineup IDs 21-30, pitcher (position P, batting_order=10) = ID 30
# Team 420 (home): lineup IDs 31-40, pitcher (position P, batting_order=10) = ID 40
away_starter = session.get(Lineup, 30) # Team 69 pitcher, player_id=30
home_starter = session.get(Lineup, 40) # Team 420 pitcher, player_id=40
# Get some batters for creating plays (batting_order=1 = leadoff hitter)
away_batter = session.get(Lineup, 21) # Team 69 leadoff hitter
home_batter = session.get(Lineup, 31) # Team 420 leadoff hitter
return game, {
"away_starter": away_starter,
"home_starter": home_starter,
"away_batter": away_batter,
"home_batter": home_batter,
}
def create_play(
self,
session: Session,
game: Game,
play_num: int,
inning_num: int,
inning_half: str,
pitcher: Lineup,
batter: Lineup,
away_score: int = 0,
home_score: int = 0,
is_go_ahead: bool = False,
is_tied: bool = False,
) -> Play:
"""Helper to create a play with specified parameters matching factory pattern."""
play = Play(
game=game,
play_num=play_num,
inning_num=inning_num,
inning_half=inning_half,
pitcher=pitcher,
batter=batter,
batter_pos=batter.position,
away_score=away_score,
home_score=home_score,
is_go_ahead=is_go_ahead,
is_tied=is_tied,
batting_order=batter.batting_order,
pa=1,
ab=1,
complete=True,
)
session.add(play)
return play
def test_basic_win_loss_home_team_wins(self, session: Session):
"""Test basic win/loss when home team wins."""
game, lineups = self.get_factory_game_and_lineups(session)
away_starter = lineups["away_starter"]
home_starter = lineups["home_starter"]
away_batter = lineups["away_batter"]
home_batter = lineups["home_batter"]
# Game where home team wins 3-2
# Away team takes lead 2-0
self.create_play(
session,
game,
1,
1,
"top",
home_starter,
away_batter,
1,
0,
is_go_ahead=True,
)
self.create_play(
session,
game,
2,
2,
"top",
home_starter,
away_batter,
2,
0,
is_go_ahead=True,
)
# Home team comes back to win 3-2
self.create_play(session, game, 3, 3, "bot", away_starter, home_batter, 2, 1)
self.create_play(
session, game, 4, 4, "bot", away_starter, home_batter, 2, 2, is_tied=True
)
self.create_play(
session,
game,
5,
5,
"bot",
away_starter,
home_batter,
2,
3,
is_go_ahead=True,
)
session.commit()
decisions = get_db_ready_decisions(session, game, 3)
decision_dict = {d["pitcher_id"]: d for d in decisions}
# Home starter should get the win (pitcher of record when team took lead)
assert decision_dict[40]["win"] == 1 # Player ID 40 = home starter
assert decision_dict[40]["loss"] == 0
# Away starter should get the loss (gave up go-ahead run)
assert decision_dict[30]["win"] == 0 # Player ID 30 = away starter
assert decision_dict[30]["loss"] == 1
def test_basic_win_loss_away_team_wins(self, session: Session):
"""Test basic win/loss when away team wins."""
game, lineups = self.setup_test_game_with_pitchers(session)
away_starter, home_starter = lineups[0], lineups[1]
# Game where away team wins 4-2
# Away team takes early lead
self.create_play(
session, game, 1, 1, "top", home_starter, 2, 0, is_go_ahead=True
)
self.create_play(
session, game, 2, 2, "top", home_starter, 4, 0, is_go_ahead=True
)
# Home team scores but doesn't catch up
self.create_play(session, game, 3, 3, "bot", away_starter, 4, 2)
session.commit()
decisions = get_db_ready_decisions(session, game, 3)
decision_dict = {d["pitcher_id"]: d for d in decisions}
# Away starter should get the win
assert decision_dict[200]["win"] == 1
assert decision_dict[200]["loss"] == 0
# Home starter should get the loss
assert decision_dict[201]["win"] == 0
assert decision_dict[201]["loss"] == 1
def test_home_team_save_situation(self, session: Session):
"""Test save situation for home team pitcher."""
game, lineups = self.setup_test_game_with_pitchers(session)
_, home_starter, _, _, _, home_closer = (
lineups[0],
lineups[1],
lineups[2],
lineups[3],
lineups[4],
lineups[5],
)
# Home team leads 3-1, closer comes in during 7th inning (final_inning = 9)
self.create_play(
session, game, 1, 1, "top", home_starter, 0, 3, is_go_ahead=True
)
self.create_play(session, game, 2, 2, "top", home_starter, 1, 3)
# Home closer enters in 7th inning (within final 3 innings)
# Lead is ≤ 3 runs, closer is not starter
self.create_play(session, game, 3, 7, "top", home_closer, 1, 3)
self.create_play(session, game, 4, 9, "top", home_closer, 1, 3) # Final inning
session.commit()
decisions = get_db_ready_decisions(session, game, 3)
decision_dict = {d["pitcher_id"]: d for d in decisions}
# Home closer should get the save
assert decision_dict[205]["is_save"] == 1
assert decision_dict[205]["hold"] == 0
def test_away_team_save_situation(self, session: Session):
"""Test save situation for away team pitcher (our new fix)."""
game, lineups = self.setup_test_game_with_pitchers(session)
away_starter, home_starter, away_reliever, _ = (
lineups[0],
lineups[1],
lineups[2],
lineups[3],
)
# Away team leads 4-1, reliever comes in during 7th inning
self.create_play(
session, game, 1, 1, "top", home_starter, 4, 0, is_go_ahead=True
)
self.create_play(session, game, 2, 2, "bot", away_starter, 4, 1)
# Away reliever enters in 7th inning bottom half
# Lead is ≤ 3 runs, reliever is not starter
self.create_play(session, game, 3, 7, "bot", away_reliever, 4, 1)
self.create_play(
session, game, 4, 9, "bot", away_reliever, 4, 1
) # Final inning
session.commit()
decisions = get_db_ready_decisions(session, game, 3)
decision_dict = {d["pitcher_id"]: d for d in decisions}
# Away reliever should get the save (this tests our new away team save logic)
assert decision_dict[10]["is_save"] == 1 # Player ID 10 = away reliever
assert decision_dict[10]["hold"] == 0
def test_hold_situation(self, session: Session):
"""Test hold situation for reliever."""
game, lineups = self.setup_test_game_with_pitchers(session)
_, home_starter, _, home_reliever, _, home_closer = (
lineups[0],
lineups[1],
lineups[2],
lineups[3],
lineups[4],
lineups[5],
)
# Home team leads, reliever enters in save situation but gets replaced
self.create_play(
session, game, 1, 1, "top", home_starter, 0, 3, is_go_ahead=True
)
# Home reliever enters in save situation (7th inning, 3-run lead)
self.create_play(session, game, 2, 7, "top", home_reliever, 1, 3)
# Home closer enters and finishes game (reliever should get hold)
self.create_play(session, game, 3, 8, "top", home_closer, 1, 3)
self.create_play(session, game, 4, 9, "top", home_closer, 1, 3)
session.commit()
decisions = get_db_ready_decisions(session, game, 3)
decision_dict = {d["pitcher_id"]: d for d in decisions}
# Home reliever should get a hold
assert decision_dict[203]["hold"] == 1
assert decision_dict[203]["is_save"] == 0
# Home closer should get the save
assert decision_dict[205]["is_save"] == 1
assert decision_dict[205]["hold"] == 0
def test_blown_save_situation(self, session: Session):
"""Test blown save situation."""
game, lineups = self.setup_test_game_with_pitchers(session)
away_starter, home_starter, _, _, _, home_closer = (
lineups[0],
lineups[1],
lineups[2],
lineups[3],
lineups[4],
lineups[5],
)
# Home team leads, closer enters in save situation but blows it
self.create_play(
session, game, 1, 1, "top", home_starter, 0, 3, is_go_ahead=True
)
# Home closer enters in save situation
self.create_play(session, game, 2, 7, "top", home_closer, 1, 3)
# Game gets tied (blown save)
self.create_play(session, game, 3, 8, "top", home_closer, 3, 3, is_tied=True)
# Home team wins it in bottom 9th
self.create_play(
session, game, 4, 9, "bot", away_starter, 3, 4, is_go_ahead=True
)
session.commit()
decisions = get_db_ready_decisions(session, game, 3)
decision_dict = {d["pitcher_id"]: d for d in decisions}
# Home closer should get a blown save
assert decision_dict[205]["b_save"] == 1
assert decision_dict[205]["is_save"] == 0
def test_save_timing_logic_fix(self, session: Session):
"""Test that save timing logic works correctly (inning_num >= final_inning - 2)."""
game, lineups = self.setup_test_game_with_pitchers(session)
_, home_starter, _, _, _, home_closer = (
lineups[0],
lineups[1],
lineups[2],
lineups[3],
lineups[4],
lineups[5],
)
# Set up game that goes 9 innings (final_inning = 9)
self.create_play(
session, game, 1, 1, "top", home_starter, 0, 2, is_go_ahead=True
)
# Closer enters in 7th inning (7 >= 9-2 = True, should qualify for save)
self.create_play(session, game, 2, 7, "top", home_closer, 1, 2)
self.create_play(session, game, 3, 9, "top", home_closer, 1, 2)
session.commit()
decisions = get_db_ready_decisions(session, game, 3)
decision_dict = {d["pitcher_id"]: d for d in decisions}
# Should get save with corrected timing logic
assert decision_dict[205]["is_save"] == 1
def test_null_winner_loser_handling(self, session: Session):
"""Test that null winner/loser doesn't crash the function."""
game, lineups = self.setup_test_game_with_pitchers(session)
away_starter, home_starter = lineups[0], lineups[1]
# Create a game with no clear go-ahead plays (edge case)
self.create_play(session, game, 1, 1, "top", home_starter, 0, 0)
self.create_play(
session, game, 2, 9, "bot", away_starter, 0, 1
) # Home wins 1-0
session.commit()
# This should not crash even if winner/loser logic has issues
decisions = get_db_ready_decisions(session, game, 3)
# Should return decisions without crashing
assert len(decisions) > 0
# Check that starter flags are set safely
decision_dict = {d["pitcher_id"]: d for d in decisions}
assert decision_dict[200]["is_start"] is True # Away starter
assert decision_dict[201]["is_start"] is True # Home starter
def test_starter_safety_checks(self, session: Session):
"""Test that null starter checks don't crash."""
game, lineups = self.setup_test_game_with_pitchers(session)
_, home_starter = lineups[0], lineups[1]
# Create basic game
self.create_play(
session, game, 1, 1, "top", home_starter, 0, 1, is_go_ahead=True
)
session.commit()
decisions = get_db_ready_decisions(session, game, 3)
decision_dict = {d["pitcher_id"]: d for d in decisions}
# Starters should be properly identified
assert decision_dict[200]["is_start"] is True
assert decision_dict[201]["is_start"] is True
assert decision_dict[200]["game_finished"] == 1 # Away finisher
assert decision_dict[201]["game_finished"] == 1 # Home finisher
def test_fallback_winner_loser_logic(self, session: Session):
"""Test improved winner/loser fallback logic using final game state."""
game, lineups = self.setup_test_game_with_pitchers(session)
away_starter, home_starter, _, _ = (
lineups[0],
lineups[1],
lineups[2],
lineups[3],
)
# Create game where go-ahead logic might not work perfectly
# but final score clearly shows winner
self.create_play(session, game, 1, 1, "top", home_starter, 1, 0)
self.create_play(
session, game, 2, 9, "bot", away_starter, 1, 3
) # Home wins 3-1
session.commit()
decisions = get_db_ready_decisions(session, game, 3)
# With fallback logic, should determine winner from final score
# Since home team won, home finisher should be winner if no other winner found
# This tests the new fallback logic we implemented
assert (
len([d for d in decisions if d.get("win", 0) == 1]) == 1
) # Exactly one winner
assert (
len([d for d in decisions if d.get("loss", 0) == 1]) == 1
) # Exactly one loser
class TestPitcherDecisionEdgeCases:
"""Test edge cases and complex scenarios for pitcher decisions."""
def test_extra_innings_decisions(self, session: Session):
"""Test decisions in extra innings game."""
# This would test the final_inning calculation for games beyond 9 innings
pass # Implement if extra innings logic is needed
def test_multiple_blown_saves_same_pitcher(self, session: Session):
"""Test pitcher who blows multiple saves in same game."""
# Edge case where same pitcher enters multiple save situations
pass # Implement if this scenario occurs
def test_starter_goes_distance_for_save(self, session: Session):
"""Test starter who goes complete game in save situation."""
# Edge case where starter != reliever save logic needs to be bypassed
pass # Implement based on specific rule requirements