"""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