import datetime import os import pytest from sqlmodel import Session, SQLModel, create_engine, select, text from testcontainers.postgres import PostgresContainer from typing import Literal from in_game.gameplay_models import BatterScouting, BattingCard, BattingRatings, Card, Cardset, Game, GameCardsetLink, Lineup, ManagerAi, PitcherScouting, PitchingCard, PitchingRatings, Play, PositionRating, RosterLink, Team, Player @pytest.fixture(name='session') def session_fixture(): # Ensure Docker socket is set for testcontainers if not os.getenv('DOCKER_HOST'): os.environ['DOCKER_HOST'] = 'unix:///home/cal/.docker/desktop/docker.sock' with PostgresContainer("postgres:13") as postgres: postgres_url = postgres.get_connection_url() engine = create_engine(postgres_url) SQLModel.metadata.create_all(engine) with Session(engine) as session: team_1 = Team( id=31, abbrev='NCB', sname='CornBelters', lname='Normal CornBelters', gmid=1234, gmname='Cal', gsheet='asdf1234', wallet=6969, team_value=69420, collection_value=169420, color='006900', season=7, event=False, career=1234, ranking=1337, has_guide=True, is_ai=True ) team_2 = Team( id=400, abbrev='WV', sname='Black Bears', lname='West Virginia Black Bears', gmid=5678, gmname='Evil Cal', gsheet='https://i.postimg.cc/HjDc8bBF/blackbears-transparent.png', wallet=350, team_value=420, collection_value=169, color='6699FF', season=7, event=False, career=2, ranking=969, has_guide=False, is_ai=False ) team_3 = Team( id=69, abbrev='NCB3', sname='CornBelters', lname='Normal CornBelters', gmid=1234, gmname='Cal', gsheet='asdf1234', wallet=6969, team_value=69420, collection_value=169420, color='006900', season=7, event=False, career=1234, ranking=1337, has_guide=True, is_ai=False ) team_4 = Team( id=420, abbrev='WV4', sname='Black Bears', lname='West Virginia Black Bears', gmid=5678, gmname='Evil Cal', gsheet='https://i.postimg.cc/HjDc8bBF/blackbears-transparent.png', wallet=350, team_value=420, collection_value=169, color='6699FF', season=7, event=False, career=2, ranking=969, has_guide=False, is_ai=True ) incomplete_team = Team( id=446, abbrev='CLS', sname='Macho Men', lname='Columbus Macho Men', gmid=181818, gmname='Mason Socc', gsheet='asdf1234', wallet=6969, team_value=69420, collection_value=169420, color='https://i.postimg.cc/8kLZCYXh/S10CLS.png', season=7, event=False, career=0 ) old_cache_team = Team( id=3, abbrev='BAL', sname='Orioles', lname='Baltimore Orioles', gmid=181818, gmname='Brandon Hyde', gsheet='asdf1234', wallet=6969, team_value=69420, collection_value=169420, color='https://i.postimg.cc/8kLZCYXh/S10CLS.png', season=7, event=False, career=0, ranking=500, has_guide=False, is_ai=False, created=datetime.datetime.today() - datetime.timedelta(days=60) ) session.add(team_1) session.add(team_2) session.add(team_3) session.add(team_4) # session.add(incomplete_team) session.add(old_cache_team) session.commit() game_1 = Game(id=1, away_team_id=31, home_team_id=400, channel_id=1234, season=9, ai_team='away', game_type='minor-league') game_2 = Game(id=2, away_team_id=69, home_team_id=420, channel_id=5678, season=9, active=False, is_pd=True, ranked=True, week=6, game_num=9, away_roster_id=69, home_roster_id=420, first_message=12345678, ai_team='home', game_type='minor-league') game_3 = Game(id=3, away_team_id=69, home_team_id=420, channel_id=5678, season=9, active=True, is_pd=True, ranked=True, week=6, game_num=10, away_roster_id=69, home_roster_id=420, first_message=34567890, ai_team='home', game_type='minor-league') session.add(game_1) session.add(game_2) session.add(game_3) session.commit() cardset_1 = Cardset(id=1, name='1969 Live') cardset_2 = Cardset(id=2, name='2024 Season') session.add(cardset_1) session.add(cardset_2) session.commit() link_1 = GameCardsetLink(game=game_1, cardset=cardset_1, priority=1) link_2 = GameCardsetLink(game=game_1, cardset=cardset_2, priority=1) link_3 = GameCardsetLink(game=game_2, cardset=cardset_1, priority=1) link_4 = GameCardsetLink(game=game_2, cardset=cardset_2, priority=2) session.add(link_1) session.add(link_2) session.add(link_3) session.add(link_4) session.commit() all_players = [] all_cards = [] all_batscouting = [] all_pitscouting = [] all_pitratings = [] all_batratings = [] all_batcards = [] all_pitcards = [] pos_list = ['C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'DH', 'P'] # Create players first for x in range(40): if x < 10: mlb_team = 'Baltimore Orioles' team_id = 31 elif x < 20: mlb_team = 'New York Yankees' team_id = 400 elif x < 30: mlb_team = 'Boston Red Sox' team_id = 69 else: mlb_team = 'Tampa Bay Rays' team_id = 420 all_players.append(Player( id=x+1, name=f'Player {x}', cost=x * 3, image=f'player_{x}_image_url', mlbclub=mlb_team, franchise=mlb_team, cardset=cardset_1 if x % 2 == 1 else cardset_2, set_num=x, rarity_id=1, pos_1=pos_list[(x % 10)], description="Live" if x % 2 == 1 else "2024" )) # Create cards and ratings for each player # Is Batter if (x + 1) % 10 != 0: all_batcards.append(BattingCard( id=x+1, steal_high=10 + (x % 10), hand='R', offense_col=1 )) all_batratings.append(BattingRatings( id=x+1, homerun=x % 10 )) all_batratings.append(BattingRatings( id=x+1001, homerun=x % 10 )) # Is Pitcher else: all_pitcards.append(PitchingCard( id=x+1, wild_pitch=x / 10, hand='R', starter_rating=5, offense_col=1 )) all_pitratings.append(PitchingRatings( id=x+1, homerun=x % 10 )) all_pitratings.append(PitchingRatings( id=x+1001, homerun=x % 10 )) all_players.append(Player( id=41, name='Player 40', cost=41*3, mlbclub='Junior All-Stars', franchise='Junior All-Stars', cardset=cardset_1, set_num=41, pos_1='DH', description='Live', created=datetime.datetime.today() - datetime.timedelta(days=60), image='player_41_image_URL', rarity_id=1 )) all_players.append(Player( id=69, name='Player 68', cost=69*3, mlbclub='Junior All-Stars', franchise='Junior All-Stars', cardset=cardset_1, set_num=69, pos_1='DH', description='Live', created=datetime.datetime.today() - datetime.timedelta(days=60), image='player_69_image_URL', rarity_id=1 )) # Add scouting data for player 41 (batter) all_batcards.append(BattingCard( id=41, steal_high=15, hand='R', offense_col=1 )) all_batratings.append(BattingRatings( id=41, homerun=5 )) all_batratings.append(BattingRatings( id=41001, homerun=5 )) all_players.append(Player( id=70, name='Player 69', cost=70*3, mlbclub='Junior All-Stars', franchise='Junior All-Stars', cardset=cardset_1, set_num=70, pos_1='SP', pos_2='DH', description='Live', created=datetime.datetime.today() - datetime.timedelta(days=60), image='player_69_pitchingcard', image2='player_69_battingcard', rarity_id=1 )) # Add scouting data for player 70 (two-way player - pitcher primary) all_pitcards.append(PitchingCard( id=70, wild_pitch=2.0, hand='R', starter_rating=7, offense_col=1 )) all_pitratings.append(PitchingRatings( id=70, homerun=3 )) all_pitratings.append(PitchingRatings( id=70001, homerun=3 )) # Also add batting data for player 70 with unique ID all_batcards.append(BattingCard( id=170, # Use unique ID to avoid conflicts steal_high=12, hand='R', offense_col=1 )) all_batratings.append(BattingRatings( id=170, homerun=4 )) all_batratings.append(BattingRatings( id=170001, homerun=4 )) # Add records in correct order to avoid foreign key constraint errors # 1. Add players first (no dependencies) for player in all_players: session.add(player) session.commit() # 2. Add batting/pitching cards and ratings (depend on players) for batcard in all_batcards: session.add(batcard) for pitcard in all_pitcards: session.add(pitcard) for batrating in all_batratings: session.add(batrating) for pitrating in all_pitratings: session.add(pitrating) session.commit() # 3. Create scouting records and cards in a coordinated way # Create cards for each player based on their position for x in range(40): if x < 10: team_id = 31 elif x < 20: team_id = 400 elif x < 30: team_id = 69 else: team_id = 420 # Is Batter if (x + 1) % 10 != 0: # Create batter scouting (let PostgreSQL assign ID) batscouting = BatterScouting( battingcard_id=x+1, ratings_vr_id=x+1, ratings_vl_id=x+1001, ) session.add(batscouting) session.commit() session.refresh(batscouting) # Get the auto-generated ID # Create card with the actual scouting ID card = Card( id=x+1, player_id=x+1, team_id=team_id, batterscouting_id=batscouting.id ) session.add(card) # Is Pitcher else: # Create pitcher scouting (let PostgreSQL assign ID) pitscouting = PitcherScouting( pitchingcard_id=x+1, ratings_vr_id=x+1, ratings_vl_id=x+1001 ) session.add(pitscouting) session.commit() session.refresh(pitscouting) # Get the auto-generated ID # Create card with the actual scouting ID card = Card( id=x+1, player_id=x+1, team_id=team_id, pitcherscouting_id=pitscouting.id ) session.add(card) # Handle special players 41 and 70 # Create batter scouting for player 41 batscouting_41 = BatterScouting( battingcard_id=41, ratings_vr_id=41, ratings_vl_id=41001, ) session.add(batscouting_41) session.commit() session.refresh(batscouting_41) card_41 = Card( id=41, player_id=41, team_id=420, created=datetime.datetime.now() - datetime.timedelta(days=60), batterscouting_id=batscouting_41.id ) session.add(card_41) # Create pitcher scouting for player 70 pitscouting_70 = PitcherScouting( pitchingcard_id=70, ratings_vr_id=70, ratings_vl_id=70001 ) session.add(pitscouting_70) session.commit() session.refresh(pitscouting_70) # Create batter scouting for player 70 (two-way player) batscouting_70 = BatterScouting( battingcard_id=170, ratings_vr_id=170, ratings_vl_id=170001, ) session.add(batscouting_70) session.commit() session.refresh(batscouting_70) card_70 = Card( id=70, player_id=70, team_id=420, pitcherscouting_id=pitscouting_70.id, batterscouting_id=batscouting_70.id ) session.add(card_70) session.commit() # Add position ratings for players (needed for manager AI decisions) all_position_ratings = [] pos_list = ['C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'DH', 'P'] for x in range(40): player_pos = pos_list[(x % 10)] # Add defensive rating for each player's primary position all_position_ratings.append(PositionRating( player_id=x+1, variant=0, position=player_pos, innings=100, range=5, error=1, arm=7 if player_pos == 'C' else None, # Catchers need arm rating pb=2 if player_pos == 'C' else None, # Catchers need passed ball rating overthrow=1 if player_pos == 'C' else None )) # Add position ratings for special players all_position_ratings.append(PositionRating( player_id=41, variant=0, position='DH', innings=100, range=5, error=1 )) all_position_ratings.append(PositionRating( player_id=69, variant=0, position='DH', innings=100, range=5, error=1 )) all_position_ratings.append(PositionRating( player_id=70, variant=0, position='SP', innings=100, range=5, error=1 )) # Also add DH rating for player 70 (two-way player) all_position_ratings.append(PositionRating( player_id=70, variant=0, position='DH', innings=50, range=4, error=2 )) for pos_rating in all_position_ratings: session.add(pos_rating) session.commit() all_lineups = [] player_count = 1 lineup_id = 1 for team in [team_1, team_2]: for (order, pos) in [(1, 'C'), (2, '1B'), (3, '2B'), (4, '3B'), (5, 'SS'), (6, 'LF'), (7, 'CF'), (8, 'RF'), (9, 'DH'), (10, 'P')]: all_lineups.append(Lineup( id=lineup_id, position=pos, batting_order=order, game=game_1, team=team, player_id=player_count, card_id=player_count )) player_count += 1 lineup_id += 1 for team in [team_3, team_4]: for (order, pos) in [(1, 'C'), (2, '1B'), (3, '2B'), (4, '3B'), (5, 'SS'), (6, 'LF'), (7, 'CF'), (8, 'RF'), (9, 'DH'), (10, 'P')]: all_lineups.append(Lineup( id=lineup_id, position=pos, batting_order=order, game=game_3, team=team, player_id=player_count, card_id=player_count )) player_count += 1 lineup_id += 1 for lineup in all_lineups: session.add(lineup) game_1_play_1 = Play( game=game_1, play_num=1, batter=all_lineups[0], batter_pos=all_lineups[0].position, pitcher=all_lineups[19], catcher=all_lineups[10], pa=1, so=1, outs=1, complete=True ) game_1_play_2 = Play( game=game_1, play_num=2, batter=all_lineups[1], batter_pos=all_lineups[1].position, pitcher=all_lineups[19], catcher=all_lineups[10], starting_outs=1 ) session.add(game_1_play_1) session.add(game_1_play_2) session.commit() g1_t1_cards = session.exec(select(Card).where(Card.team_id == 31)).all() g1_t2_cards = session.exec(select(Card).where(Card.team_id == 400)).all() g2_t1_cards = session.exec(select(Card).where(Card.team_id == 69)).all() g2_t2_cards = session.exec(select(Card).where(Card.team_id == 420)).all() for card in [*g1_t1_cards, *g1_t2_cards]: session.add(RosterLink( game_id=1, card_id=card.id, team_id=card.team_id )) for card in [*g2_t1_cards, *g2_t2_cards]: session.add(RosterLink( game_id=3, card_id=card.id, team_id=card.team_id )) # session.add(RosterLink( # game_id=3, # card_id=12, # team_id=420 # )) session.commit() # Create ManagerAi objects with explicit IDs for test compatibility manager_ai_1 = ManagerAi(id=1, name='Balanced') manager_ai_2 = ManagerAi(id=2, name='Yolo', steal=10, running=10, hold=5, catcher_throw=10, uncapped_home=10, uncapped_third=10, uncapped_trail=10, bullpen_matchup=3, behind_aggression=10, ahead_aggression=10, decide_throw=10) manager_ai_3 = ManagerAi(id=3, name='Safe', steal=3, running=3, hold=8, catcher_throw=5, uncapped_home=5, uncapped_third=3, uncapped_trail=5, bullpen_matchup=8, behind_aggression=5, ahead_aggression=1, decide_throw=1) session.add(manager_ai_1) session.add(manager_ai_2) session.add(manager_ai_3) session.commit() # Reset sequences so auto-generated IDs don't conflict with factory data session.exec(text("SELECT setval(pg_get_serial_sequence('lineup', 'id'), COALESCE((SELECT MAX(id) FROM lineup), 1))")) session.exec(text("SELECT setval(pg_get_serial_sequence('play', 'id'), COALESCE((SELECT MAX(id) FROM play), 1))")) session.commit() yield session @pytest.fixture def sample_ratings_query(): """Factory method for sample ratings query data used in batter scouting tests.""" return { "count": 2, "ratings": [ { "id": 7673, "battingcard": { "id": 3837, "player": { "player_id": 395, "p_name": "Cedric Mullins", "cost": 256, "image": "https://pd.manticorum.com/api/v2/players/395/battingcard?d=2023-11-19", "image2": None, "mlbclub": "Baltimore Orioles", "franchise": "Baltimore Orioles", "cardset": { "id": 1, "name": "2021 Season", "description": "Cards based on the full 2021 season", "event": None, "for_purchase": True, "total_cards": 791, "in_packs": True, "ranked_legal": False }, "set_num": 395, "rarity": { "id": 2, "value": 3, "name": "All-Star", "color": "FFD700" }, "pos_1": "CF", "pos_2": None, "pos_3": None, "pos_4": None, "pos_5": None, "pos_6": None, "pos_7": None, "pos_8": None, "headshot": "https://www.baseball-reference.com/req/202206291/images/headshots/2/24bf4355_mlbam.jpg", "vanity_card": None, "strat_code": "17929", "bbref_id": "mullice01", "fangr_id": None, "description": "2021", "quantity": 999, "mlbplayer": { "id": 396, "first_name": "Cedric", "last_name": "Mullins", "key_fangraphs": 17929, "key_bbref": "mullice01", "key_retro": "mullc002", "key_mlbam": 656775, "offense_col": 1 } }, "variant": 0, "steal_low": 9, "steal_high": 12, "steal_auto": True, "steal_jump": 0.2222222222222222, "bunting": "C", "hit_and_run": "B", "running": 13, "offense_col": 1, "hand": "L" }, "vs_hand": "L", "pull_rate": 0.43888889, "center_rate": 0.32777778, "slap_rate": 0.23333333, "homerun": 2.25, "bp_homerun": 5.0, "triple": 0.0, "double_three": 0.0, "double_two": 2.4, "double_pull": 7.2, "single_two": 4.6, "single_one": 3.5, "single_center": 5.85, "bp_single": 5.0, "hbp": 2.0, "walk": 9.0, "strikeout": 23.0, "lineout": 3.0, "popout": 6.0, "flyout_a": 0.0, "flyout_bq": 0.15, "flyout_lf_b": 2.8, "flyout_rf_b": 3.75, "groundout_a": 0.0, "groundout_b": 9.0, "groundout_c": 13.5, "avg": 0.2851851851851852, "obp": 0.387037037037037, "slg": 0.5060185185185185 }, { "id": 7674, "battingcard": { "id": 3837, "player": { "player_id": 395, "p_name": "Cedric Mullins", "cost": 256, "image": "https://pd.manticorum.com/api/v2/players/395/battingcard?d=2023-11-19", "image2": None, "mlbclub": "Baltimore Orioles", "franchise": "Baltimore Orioles", "cardset": { "id": 1, "name": "2021 Season", "description": "Cards based on the full 2021 season", "event": None, "for_purchase": True, "total_cards": 791, "in_packs": True, "ranked_legal": False }, "set_num": 395, "rarity": { "id": 2, "value": 3, "name": "All-Star", "color": "FFD700" }, "pos_1": "CF", "pos_2": None, "pos_3": None, "pos_4": None, "pos_5": None, "pos_6": None, "pos_7": None, "pos_8": None, "headshot": "https://www.baseball-reference.com/req/202206291/images/headshots/2/24bf4355_mlbam.jpg", "vanity_card": None, "strat_code": "17929", "bbref_id": "mullice01", "fangr_id": None, "description": "2021", "quantity": 999, "mlbplayer": { "id": 396, "first_name": "Cedric", "last_name": "Mullins", "key_fangraphs": 17929, "key_bbref": "mullice01", "key_retro": "mullc002", "key_mlbam": 656775, "offense_col": 1 } }, "variant": 0, "steal_low": 9, "steal_high": 12, "steal_auto": True, "steal_jump": 0.2222222222222222, "bunting": "C", "hit_and_run": "B", "running": 13, "offense_col": 1, "hand": "L" }, "vs_hand": "R", "pull_rate": 0.43377483, "center_rate": 0.32119205, "slap_rate": 0.24503311, "homerun": 2.0, "bp_homerun": 7.0, "triple": 0.0, "double_three": 0.0, "double_two": 2.7, "double_pull": 7.95, "single_two": 3.3, "single_one": 0.0, "single_center": 8.6, "bp_single": 5.0, "hbp": 1.0, "walk": 12.9, "strikeout": 11.1, "lineout": 8.0, "popout": 11.0, "flyout_a": 0.0, "flyout_bq": 0.4, "flyout_lf_b": 4.05, "flyout_rf_b": 6.0, "groundout_a": 0.0, "groundout_b": 3.0, "groundout_c": 14.0, "avg": 0.2828703703703704, "obp": 0.4115740740740741, "slg": 0.5342592592592592 } ] } def create_incomplete_team(session: Session): """Factory method for creating an incomplete team for constraint testing.""" return Team( id=446, abbrev='CLS', sname='Macho Men', lname='Columbus Macho Men', gmid=181818, gmname='Mason Socc', gsheet='asdf1234', wallet=6969, team_value=69420, collection_value=169420, color='https://i.postimg.cc/8kLZCYXh/S10CLS.png', season=7, event=False, career=0 ) def create_sample_play_for_scorebug(session: Session): """Factory method for creating a play object for scorebug testing.""" return Play( game_id=3, play_num=69, batter=session.get(Lineup, 1), batter_pos='DH', pitcher=session.get(Lineup, 20), catcher=session.get(Lineup, 11), starting_outs=1, inning_num=6 ) def get_next_play_id(session: Session): """Get the next available Play ID for SQLite tests.""" from sqlmodel import func max_id = session.exec(select(func.max(Play.id))).one() return (max_id or 0) + 1