paper-dynasty-discord/tests/in_game/test_pitcher_decisions.py
2025-08-17 08:46:55 -05:00

316 lines
15 KiB
Python

"""Tests for pitcher decision logic in get_db_ready_decisions."""
import pytest
from sqlmodel import Session
from in_game.gameplay_queries import get_db_ready_decisions
from in_game.gameplay_models import Game, Team, Player, Lineup, Play, Cardset, Card
from tests.factory import session_fixture
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)
away_starter, 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)
away_starter, 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)
away_starter, 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)
away_starter, 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, _, home_reliever = 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)
decision_dict = {d['pitcher_id']: d for d in decisions}
# 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