import asyncio import copy import logging import discord from discord import SelectOption import pandas as pd from sqlmodel import Session, or_, select, func from sqlalchemy import delete from typing import Literal from api_calls import db_delete, db_get, db_post from dice import d_twenty_roll, frame_plate_check, sa_fielding_roll from exceptions import ( CardLegalityException, CardNotFoundException, DatabaseError, GameException, GameNotFoundException, GoogleSheetsException, InvalidResultException, LineupsMissingException, log_errors, log_exception, MissingRosterException, NoPlayerResponseException, PlayerNotFoundException, PlayInitException, PlayLockedException, PlayNotFoundException, PositionNotFoundException, TeamNotFoundException, ) from gauntlets import post_result from helpers import ( COLORS, DEFENSE_LITERAL, DEFENSE_NO_PITCHER_LITERAL, get_channel, position_name_to_abbrev, ) from in_game.game_helpers import PUBLIC_FIELDS_CATEGORY_NAME, legal_check from in_game.gameplay_models import ( BattingCard, Card, Game, Lineup, PositionRating, RosterLink, Team, Play, ) from in_game.gameplay_queries import ( get_active_games_by_team, get_available_batters, get_batter_card, get_batting_statline, get_game_cardset_links, get_or_create_ai_card, get_pitcher_runs_by_innings, get_pitching_statline, get_position, get_available_pitchers, get_card_or_none, get_channel_game_or_none, get_db_ready_decisions, get_db_ready_plays, get_game_lineups, get_last_team_play, get_one_lineup, get_player_id_from_dict, get_player_name_from_dict, get_player_or_none, get_sorted_lineups, get_team_or_none, get_players_last_pa, post_game_rewards, ) from utilities.buttons import ButtonOptions, Confirm, ask_confirm, ask_with_buttons from utilities.dropdown import ( DropdownView, SelectBatterSub, SelectDefensiveChange, SelectReliefPitcher, SelectStartingPitcher, SelectViewDefense, ) from utilities.embeds import image_embed from utilities.pages import Pagination logger = logging.getLogger("discord_app") WPA_DF = pd.read_csv("storage/wpa_data.csv").set_index("index") TO_BASE = {2: "to second", 3: "to third", 4: "home"} def safe_wpa_lookup( inning_half: str, inning_num: int, starting_outs: int, on_base_code: int, run_diff: int, ) -> float: """ Safely lookup win probability from WPA_DF with fallback logic for missing keys. Fallback strategy: 1. Try exact lookup 2. Try with simplified on_base_code (reduce to bases empty state) 3. Return 0.0 if no data available Args: inning_half: 'top' or 'bot' inning_num: Inning number (1-9) starting_outs: Number of outs (0-2) on_base_code: Base occupation code (0-7) run_diff: Home run differential (-6 to 6) Returns: float: Home team win expectancy (0.0 to 1.0) """ # Construct the primary key key = f"{inning_half}_{inning_num}_{starting_outs}_out_{on_base_code}_obc_{run_diff}_home_run_diff" # Try exact lookup first try: return float(WPA_DF.loc[key, "home_win_ex"]) except KeyError: logger.warning(f"WPA key not found: {key}, attempting fallback") # Fallback 1: Try with simplified on_base_code (bases empty = 0) if on_base_code != 0: fallback_key = f"{inning_half}_{inning_num}_{starting_outs}_out_0_obc_{run_diff}_home_run_diff" try: result = float(WPA_DF.loc[fallback_key, "home_win_ex"]) logger.info( f"WPA fallback successful using bases empty state: {fallback_key}" ) return result except KeyError: logger.warning(f"WPA fallback key not found: {fallback_key}") # Fallback 2: Return 0.0 if no data available logger.warning(f"WPA using fallback value 0.0 for missing key: {key}") return 0.0 AT_BASE = {2: "at second", 3: "at third", 4: "at home"} RANGE_CHECKS = {1: 3, 2: 7, 3: 11, 4: 15, 5: 19} async def get_scorebug_embed( session: Session, this_game: Game, full_length: bool = True, classic: bool = True, live_scorecard: bool = False, ) -> discord.Embed: gt_string = " - Unlimited" if this_game.game_type == "minor-league": gt_string = " - Minor League" elif this_game.game_type == "major-league": gt_string = " - Major League" elif this_game.game_type == "hall-of-fame": gt_string = " - Hall of Fame" elif "gauntlet" in this_game.game_type: gt_string = " - Gauntlet" elif "flashback" in this_game.game_type: gt_string = " - Flashback" elif "exhibition" in this_game.game_type: gt_string = " - Exhibition" logger.info(f"get_scorebug_embed - this_game: {this_game} / gt_string: {gt_string}") curr_play = this_game.current_play_or_none(session) embed = discord.Embed( title=f"{this_game.away_team.sname} @ {this_game.home_team.sname}{gt_string}" ) if curr_play is None: logger.info( f"There is no play in game {this_game.id}, trying to initialize play" ) try: curr_play = this_game.initialize_play(session) except LineupsMissingException: logger.info("get_scorebug_embed - Could not initialize play") if curr_play is not None: embed.add_field(name="Game State", value=curr_play.scorebug_ascii, inline=False) logger.info(f"curr_play: {curr_play}") if curr_play.pitcher.is_fatigued: embed_color = COLORS["red"] elif curr_play.pitcher.after_play == curr_play.play_num - 1: embed_color = COLORS["white"] elif curr_play.is_new_inning: embed_color = COLORS["yellow"] else: embed_color = COLORS["sba"] embed.color = embed_color def steal_string(batting_card: BattingCard) -> str: steal_string = "-/- (---)" if batting_card.steal_jump > 0: jump_chances = round(batting_card.steal_jump * 36) if jump_chances == 6: good_jump = 7 elif jump_chances == 5: good_jump = 6 elif jump_chances == 4: good_jump = 5 elif jump_chances == 3: good_jump = 4 elif jump_chances == 2: good_jump = 3 elif jump_chances == 1: good_jump = 2 elif jump_chances == 7: good_jump = "4,5" elif jump_chances == 8: good_jump = "4,6" elif jump_chances == 9: good_jump = "3-5" elif jump_chances == 10: good_jump = "2-5" elif jump_chances == 11: good_jump = "6,7" elif jump_chances == 12: good_jump = "4-6" elif jump_chances == 13: good_jump = "2,4-6" elif jump_chances == 14: good_jump = "3-6" elif jump_chances == 15: good_jump = "2-6" elif jump_chances == 16: good_jump = "2,5-6" elif jump_chances == 17: good_jump = "3,5-6" elif jump_chances == 18: good_jump = "4-6" elif jump_chances == 19: good_jump = "2,4-7" elif jump_chances == 20: good_jump = "3-7" elif jump_chances == 21: good_jump = "2-7" elif jump_chances == 22: good_jump = "2-7,12" elif jump_chances == 23: good_jump = "2-7,11" elif jump_chances == 24: good_jump = "2,4-8" elif jump_chances == 25: good_jump = "3-8" elif jump_chances == 26: good_jump = "2-8" elif jump_chances == 27: good_jump = "2-8,12" elif jump_chances == 28: good_jump = "2-8,11" elif jump_chances == 29: good_jump = "3-9" elif jump_chances == 30: good_jump = "2-9" elif jump_chances == 31: good_jump = "2-9,12" elif jump_chances == 32: good_jump = "2-9,11" elif jump_chances == 33: good_jump = "2-10" elif jump_chances == 34: good_jump = "3-11" elif jump_chances == 35: good_jump = "2-11" else: good_jump = "2-12" steal_string = f"{'`*`' if batting_card.steal_auto else ''}{good_jump}/- ({batting_card.steal_high}-{batting_card.steal_low})" return steal_string baserunner_string = "" if curr_play.on_first is not None: runcard = curr_play.on_first.card.batterscouting.battingcard baserunner_string += f"On First: {curr_play.on_first.player.name_card_link('batting')}\nSteal: {steal_string(runcard)}, Run: {runcard.running}\n" if curr_play.on_second is not None: runcard = curr_play.on_second.card.batterscouting.battingcard baserunner_string += f"On Second: {curr_play.on_second.player.name_card_link('batting')}\nSteal: {steal_string(runcard)}, Run: {runcard.running}\n" if curr_play.on_third is not None: runcard = curr_play.on_third.card.batterscouting.battingcard baserunner_string += f"On Third: {curr_play.on_third.player.name_card_link('batting')}\nSteal: {steal_string(runcard)}, Run: {runcard.running}\n" logger.info( f"gameplay_models - get_scorebug_embed - baserunner_string: {baserunner_string}" ) pitchingcard = curr_play.pitcher.card.pitcherscouting.pitchingcard battingcard = curr_play.batter.card.batterscouting.battingcard pit_string = f"{pitchingcard.hand.upper()}HP | {curr_play.pitcher.player.name_card_link('pitching')}" if curr_play.pitcher.is_fatigued: pit_string += "\n***F A T I G U E D***" if len(baserunner_string) > 0: logger.info("Adding pitcher hold to scorebug") pitchingcard = curr_play.pitcher.card.pitcherscouting.pitchingcard pit_string += f"\nHold: {'+' if pitchingcard.hold > 0 else ''}{pitchingcard.hold}, WP: {pitchingcard.wild_pitch}, Bk: {pitchingcard.balk}" # battingcard = curr_play.batter.card.batterscouting.battingcard # bat_string += f'\nBunt: {battingcard.bunting}, HnR: {battingcard.hit_and_run}' pit_string += f"\n{get_pitching_statline(session, curr_play.pitcher)}" embed.add_field(name="Pitcher", value=pit_string) bat_string = f"{curr_play.batter.batting_order}. {battingcard.hand.upper()} | {curr_play.batter.player.name_card_link('batting')}\n{get_batting_statline(session, curr_play.batter)}" embed.add_field(name="Batter", value=bat_string) logger.info("Setting embed image to batter card") embed.set_image(url=curr_play.batter.player.batter_card_url) if len(baserunner_string) > 0: logger.info("Adding baserunner info to embed") embed.add_field(name=" ", value=" ", inline=False) embed.add_field(name="Baserunners", value=baserunner_string) c_query = session.exec( select(PositionRating).where( PositionRating.player_id == curr_play.catcher.card.player.id, PositionRating.position == "C", PositionRating.variant == curr_play.catcher.card.variant, ) ).all() if len(c_query) > 0: catcher_rating = c_query[0] else: log_exception( PositionNotFoundException, f"No catcher rating found for {curr_play.catcher.card.player.name}", ) cat_string = f"{curr_play.catcher.player.name_card_link('batter')}\nArm: {'+' if catcher_rating.arm > 0 else ''}{catcher_rating.arm}, PB: {catcher_rating.pb}, OT: {catcher_rating.overthrow}" embed.add_field(name="Catcher", value=cat_string) if curr_play.ai_is_batting and curr_play.on_base_code > 0: logger.info("Checking on baserunners for jump") if curr_play.on_base_code in [2, 4]: to_base = 3 elif curr_play.on_base_code in [1, 5]: to_base = 2 else: to_base = 4 jump_resp = curr_play.managerai.check_jump( session, this_game, to_base=to_base ) ai_note = jump_resp.ai_note else: logger.info("Checking defensive alignment") def_align = curr_play.managerai.defense_alignment(session, this_game) logger.info(f"def_align: {def_align}") ai_note = def_align.ai_note logger.info(f"gameplay_models - get_scorebug_embed - ai_note: {ai_note}") if len(ai_note) > 0 and not live_scorecard: gm_name = ( this_game.home_team.gmname if this_game.ai_team == "home" else this_game.away_team.gmname ) embed.add_field(name=f"{gm_name} will...", value=ai_note, inline=False) else: embed.add_field(name=" ", value=" ", inline=False) if full_length: embed.add_field( name=f"{this_game.away_team.abbrev} Lineup", value=this_game.team_lineup( session, this_game.away_team, with_links=False ), ) embed.add_field( name=f"{this_game.home_team.abbrev} Lineup", value=this_game.team_lineup( session, this_game.home_team, with_links=False ), ) else: logger.info(f"There is no play in game {this_game.id}, posting lineups") logger.info("Pulling away lineup") embed.add_field( name=f"{this_game.away_team.abbrev} Lineup", value=this_game.team_lineup(session, this_game.away_team), ) logger.info("Pulling home lineup") embed.add_field( name=f"{this_game.home_team.abbrev} Lineup", value=this_game.team_lineup(session, this_game.home_team), ) return embed async def new_game_checks( session: Session, interaction: discord.Interaction, away_team_abbrev: str, home_team_abbrev: str, ): try: logger.info(f"Checking for game conflicts in {interaction.channel.name}") await new_game_conflicts(session, interaction) except GameException: return None logger.info("Getting teams") away_team = await get_team_or_none(session, team_abbrev=away_team_abbrev) if away_team is None: logger.error("Away team not found") await interaction.edit_original_response( content=f"Hm. I'm not sure who **{away_team_abbrev}** is - check on that and try again!" ) return None home_team = await get_team_or_none(session, team_abbrev=home_team_abbrev) if home_team is None: logger.error("Home team not found") await interaction.edit_original_response( content=f"Hm. I'm not sure who **{home_team_abbrev}** is - check on that and try again!" ) return None human_team = away_team if home_team.is_ai else home_team logger.info("Checking for other team games") conflict_games = get_active_games_by_team(session, team=human_team) if len(conflict_games) > 0: logger.error( f"Conflict creating a new game in channel: {interaction.channel.name}" ) await interaction.edit_original_response( content=f"Ope. The {human_team.sname} are already playing over in {interaction.guild.get_channel(conflict_games[0].channel_id).mention}" ) return None if interaction.user.id not in [away_team.gmid, home_team.gmid]: if interaction.user.id != 258104532423147520: await interaction.edit_original_response( content="You can only start a new game if you GM one of the teams." ) return None else: await interaction.channel.send( "Sigh. Cal is cheating again starting a game for someone else." ) return {"away_team": away_team, "home_team": home_team} def starting_pitcher_dropdown_view( session: Session, this_game: Game, human_team: Team, game_type: str = None, responders: list[discord.User] = None, ): pitchers = get_available_pitchers( session, this_game, human_team, sort="starter-desc" ) logger.info(f"sorted pitchers: {pitchers}") if len(pitchers) == 0: log_exception(MissingRosterException, "No pitchers were found to select SP") sp_selection = SelectStartingPitcher( this_game=this_game, this_team=human_team, session=session, league_name=this_game.game_type if game_type is None else game_type, options=[ SelectOption( label=f"{x.player.name_with_desc} (S{x.pitcherscouting.pitchingcard.starter_rating}/R{x.pitcherscouting.pitchingcard.relief_rating})", value=x.id, ) for x in pitchers ], placeholder="Select your starting pitcher", responders=responders, ) return DropdownView(dropdown_objects=[sp_selection]) def relief_pitcher_dropdown_view( session: Session, this_game: Game, human_team: Team, batting_order: int, responders: list[discord.User] = None, ): pitchers = get_available_pitchers(session, this_game, human_team) logger.info(f"sorted pitchers: {pitchers}") if len(pitchers) == 0: log_exception(MissingRosterException, "No pitchers were found to select RP") rp_selection = SelectReliefPitcher( this_game=this_game, this_team=human_team, batting_order=batting_order, session=session, options=[ SelectOption( label=f"{x.player.name_with_desc} (S{x.pitcherscouting.pitchingcard.starter_rating}/R{x.pitcherscouting.pitchingcard.relief_rating})", value=x.id, ) for x in pitchers ], placeholder="Select your relief pitcher", responders=responders, ) return DropdownView(dropdown_objects=[rp_selection]) async def defender_dropdown_view( session: Session, this_game: Game, human_team: Team, new_position: DEFENSE_NO_PITCHER_LITERAL, responders: list[discord.User] = None, ): active_players = get_game_lineups(session, this_game, human_team, is_active=True) first_pass = [ x for x in active_players if x.position != "P" and x.batting_order != 10 ] if len(first_pass) == 0: log_exception( MissingRosterException, "No active defenders were found to make defensive change", ) defender_list = [] for x in first_pass: this_pos = session.exec( select(PositionRating).where( PositionRating.player_id == x.player.id, PositionRating.position == position_name_to_abbrev(new_position), PositionRating.variant == x.card.variant, ) ).all() if len(this_pos) > 0: defender_list.append(x) if len(defender_list) == 0: log_exception( PlayerNotFoundException, f"I dont see any legal defenders for {new_position} on the field.", ) defender_selection = SelectDefensiveChange( this_game=this_game, this_team=human_team, new_position=new_position, session=session, responders=responders, placeholder=f"Who is moving to {new_position}?", options=[ SelectOption(label=f"{x.player.name_with_desc}", value=x.id) for x in defender_list ], ) return DropdownView(dropdown_objects=[defender_selection]) def sub_batter_dropdown_view( session: Session, this_game: Game, human_team: Team, batting_order: int, responders: list[discord.User], ): batters = get_available_batters(session, this_game, human_team) logger.info(f"batters: {batters}") bat_selection = SelectBatterSub( this_game=this_game, this_team=human_team, session=session, batting_order=batting_order, options=[ SelectOption( label=f"{x.batterscouting.battingcard.hand.upper()} | {x.player.name_with_desc}", value=x.id, ) for x in batters ], placeholder="Select your Sub", responders=responders, ) return DropdownView(dropdown_objects=[bat_selection]) async def read_lineup( session: Session, interaction: discord.Interaction, this_game: Game, lineup_team: Team, sheets_auth, lineup_num: int, league_name: str, ): """ Commits lineups and rosterlinks """ existing_lineups = get_game_lineups( session=session, this_game=this_game, specific_team=lineup_team, is_active=True ) if len(existing_lineups) > 1: await interaction.edit_original_response( f"It looks like the {lineup_team.sname} already have a lineup. Run `/substitute` to make changes." ) return await interaction.edit_original_response( content="Okay, let's put this lineup card together..." ) session.add(this_game) human_lineups = await get_lineups_from_sheets( session, sheets_auth, this_game, this_team=lineup_team, lineup_num=lineup_num, roster_num=( this_game.away_roster_id if this_game.home_team.is_ai else this_game.home_roster_id ), ) await interaction.edit_original_response( content="Heard from sheets, pulling in scouting data..." ) for batter in human_lineups: session.add(batter) session.commit() for batter in human_lineups: if batter.position != "DH": try: await get_position(session, batter.card, batter.position) except PositionNotFoundException: raise PositionNotFoundException( f"Could not find {batter.position} ratings for **{batter.player.name_with_desc}**. " f"Please check your lineup in Google Sheets and make sure each player is at a valid position." ) return this_game.initialize_play(session) def get_obc(on_first=None, on_second=None, on_third=None) -> int: if on_third is not None: if on_second is not None: if on_first is not None: obc = 7 else: obc = 6 elif on_first is not None: obc = 5 else: obc = 3 elif on_second is not None: if on_first is not None: obc = 4 else: obc = 2 elif on_first is not None: obc = 1 else: obc = 0 return obc def get_re24( this_play: Play, runs_scored: int, new_obc: int, new_starting_outs: int ) -> float: re_data = { 0: [0.457, 0.231, 0.077], 1: [0.793, 0.438, 0.171], 2: [1.064, 0.596, 0.259], 4: [1.373, 0.772, 0.351], 3: [1.340, 0.874, 0.287], 5: [1.687, 1.042, 0.406], 6: [1.973, 1.311, 0.448], 7: [2.295, 1.440, 0.618], } start_re24 = re_data[this_play.on_base_code][this_play.starting_outs] end_re24 = ( 0 if this_play.starting_outs + this_play.outs > 2 else re_data[new_obc][new_starting_outs] ) return round(end_re24 - start_re24 + runs_scored, 3) def get_wpa(this_play: Play, next_play: Play): """ Returns wpa relative to batting team of this_play. Negative value if bad play, positive value if good play. """ new_rd = next_play.home_score - next_play.away_score if new_rd > 6: new_rd = 6 elif new_rd < -6: new_rd = -6 old_rd = this_play.home_score - this_play.away_score if old_rd > 6: old_rd = 6 elif old_rd < -6: old_rd = -6 # print(f'get_wpa: new_rd = {new_rd} / old_rd = {old_rd}') if ( next_play.inning_num >= 9 and new_rd > 0 and next_play.inning_half == "bot" ) or (next_play.inning_num > 9 and new_rd > 0 and next_play.is_new_inning): # print(f'manually setting new_win_ex to 1.0') new_win_ex = 1.0 else: inning_num = 9 if next_play.inning_num > 9 else next_play.inning_num new_win_ex = safe_wpa_lookup( next_play.inning_half, inning_num, next_play.starting_outs, next_play.on_base_code, new_rd, ) # print(f'new_win_ex = {new_win_ex}') inning_num = 9 if this_play.inning_num > 9 else this_play.inning_num old_win_ex = safe_wpa_lookup( this_play.inning_half, inning_num, this_play.starting_outs, this_play.on_base_code, old_rd, ) # print(f'old_win_ex = {old_win_ex}') wpa = float(round(new_win_ex - old_win_ex, 3)) # print(f'final wpa: {wpa}') if this_play.inning_half == "top": return wpa * -1.0 return wpa def complete_play(session: Session, this_play: Play): """ Commits this_play and new_play """ logger.info(f"Completing play {this_play.id} in game {this_play.game.id}") nso = this_play.starting_outs + this_play.outs runs_scored = 0 on_first, on_second, on_third = None, None, None logger.info("Running bulk checks") if nso >= 3: switch_sides = True obc = 0 nso = 0 nih = "bot" if this_play.inning_half == "top" else "top" away_score = this_play.away_score home_score = this_play.home_score try: opponent_play = get_last_team_play( session, this_play.game, this_play.pitcher.team ) nbo = ( opponent_play.batting_order + 1 if opponent_play.pa == 1 else opponent_play.batting_order ) except PlayNotFoundException: logger.info( f"logic_gameplay - complete_play - No last play found for {this_play.pitcher.team.sname}, setting upcoming batting order to 1" ) nbo = 1 finally: new_batter_team = ( this_play.game.away_team if nih == "top" else this_play.game.home_team ) new_pitcher_team = ( this_play.game.away_team if nih == "bot" else this_play.game.home_team ) inning = this_play.inning_num if nih == "bot" else this_play.inning_num + 1 logger.info("Calculate runs scored") for this_runner, runner_dest in [ (this_play.batter, this_play.batter_final), (this_play.on_first, this_play.on_first_final), (this_play.on_second, this_play.on_second_final), (this_play.on_third, this_play.on_third_final), ]: if runner_dest is not None: if runner_dest == 1: logger.info(f"{this_runner} advances to first") if on_first is not None: log_exception( ValueError, f"Cannot place {this_runner.player.name} on first; {on_first.player.name} is already placed there", ) if not switch_sides: on_first = this_runner elif runner_dest == 2: logger.info(f"{this_runner} advances to second") if on_second is not None: log_exception( ValueError, f"Cannot place {this_runner.player.name} on second; {on_second.player.name} is already placed there", ) if not switch_sides: on_second = this_runner elif runner_dest == 3: logger.info(f"{this_runner} advances to third") if on_third is not None: log_exception( ValueError, f"Cannot place {this_runner.player.name} on third; {on_third.player.name} is already placed there", ) if not switch_sides: on_third = this_runner elif runner_dest == 4: logger.info(f"{this_runner} advances to home") runs_scored += 1 else: switch_sides = False nbo = ( this_play.batting_order + 1 if this_play.pa == 1 else this_play.batting_order ) nih = this_play.inning_half new_batter_team = this_play.batter.team new_pitcher_team = this_play.pitcher.team inning = this_play.inning_num logger.info("Calculate runs scored") for this_runner, runner_dest in [ (this_play.batter, this_play.batter_final), (this_play.on_first, this_play.on_first_final), (this_play.on_second, this_play.on_second_final), (this_play.on_third, this_play.on_third_final), ]: if runner_dest is not None: if runner_dest == 1: logger.info(f"{this_runner} advances to first") if on_first is not None: log_exception( ValueError, f"Cannot place {this_runner.player.name} on first; {on_first.player.name} is already placed there", ) if not switch_sides: on_first = this_runner elif runner_dest == 2: logger.info(f"{this_runner} advances to second") if on_second is not None: log_exception( ValueError, f"Cannot place {this_runner.player.name} on second; {on_second.player.name} is already placed there", ) if not switch_sides: on_second = this_runner elif runner_dest == 3: logger.info(f"{this_runner} advances to third") if on_third is not None: log_exception( ValueError, f"Cannot place {this_runner.player.name} on third; {on_third.player.name} is already placed there", ) if not switch_sides: on_third = this_runner elif runner_dest == 4: logger.info(f"{this_runner} advances to home") runs_scored += 1 obc = get_obc(on_first, on_second, on_third) if this_play.inning_half == "top": away_score = this_play.away_score + runs_scored home_score = this_play.home_score logger.info("Check for go-ahead run") if ( runs_scored > 0 and this_play.away_score <= this_play.home_score and away_score > home_score ): this_play.is_go_ahead = True else: away_score = this_play.away_score home_score = this_play.home_score + runs_scored logger.info("Check for go-ahead run") if ( runs_scored > 0 and this_play.home_score <= this_play.away_score and home_score > away_score ): this_play.is_go_ahead = True logger.info("Calculating re24") this_play.re24 = get_re24( this_play, runs_scored, new_obc=obc, new_starting_outs=nso ) if nbo > 9: nbo = 1 new_batter = get_one_lineup( session, this_play.game, new_batter_team, batting_order=nbo ) logger.info(f"new_batter: {new_batter}") new_pitcher = get_one_lineup( session, this_play.game, new_pitcher_team, position="P" ) logger.info(f"Check for {new_pitcher.player.name} POW") outs = session.exec( select(func.sum(Play.outs)).where( Play.game == this_play.game, Play.pitcher == new_pitcher ) ).one() if outs is None: outs = 0 logger.info(f"Outs recorded: {outs}") if new_pitcher.replacing_id is None: pow_outs = new_pitcher.card.pitcherscouting.pitchingcard.starter_rating * 3 logger.info(f"Using starter rating, POW outs: {pow_outs}") else: pow_outs = new_pitcher.card.pitcherscouting.pitchingcard.relief_rating * 3 logger.info(f"Using relief rating, POW outs: {pow_outs}") if not new_pitcher.is_fatigued: logger.info("Pitcher is not currently fatigued") if outs >= pow_outs: logger.info("Pitcher is beyond POW - adding fatigue") new_pitcher.is_fatigued = True elif new_pitcher.replacing_id is None: logger.info("Pitcher is not in POW yet") total_runs = session.exec( select(func.count(Play.id)).where( Play.game == this_play.game, Play.pitcher == new_pitcher, Play.run == 1, ) ).one() logger.info( f"Runs allowed by {new_pitcher.player.name_with_desc}: {total_runs}" ) if total_runs >= 5: if total_runs >= 7: logger.info("Starter has allowed 7+ runs - adding fatigue") new_pitcher.is_fatigued = True else: last_two = [ x for x in range( this_play.inning_num, this_play.inning_num - 2, -1 ) if x > 0 ] runs_last_two = get_pitcher_runs_by_innings( session, this_play.game, new_pitcher, last_two ) logger.info(f"Runs allowed last two innings: {runs_last_two}") if runs_last_two >= 6: logger.info( "Starter has allowed at least six in the last two - adding fatigue" ) new_pitcher.is_fatigued = True else: runs_this_inning = get_pitcher_runs_by_innings( session, this_play.game, new_pitcher, [this_play.inning_num] ) logger.info(f"Runs allowed this inning: {runs_this_inning}") if runs_this_inning >= 5: logger.info( "Starter has allowed at least five this inning - adding fatigue" ) new_pitcher.is_fatigued = True session.add(new_pitcher) if outs >= (pow_outs - 3): in_pow = True if not new_pitcher.is_fatigued: logger.info("checking for runners in POW") runners_in_pow = session.exec( select(func.count(Play.id)).where( Play.game == this_play.game, Play.pitcher == new_pitcher, Play.in_pow == True, # noqa: E712 or_(Play.hit == 1, Play.bb == 1), Play.ibb == 0, ) ).one() # change to hits and walks logger.info(f"runners in pow: {runners_in_pow}") if runners_in_pow >= 3: new_pitcher.is_fatigued = True else: in_pow = False new_play = Play( game=this_play.game, play_num=this_play.play_num + 1, batting_order=nbo, inning_half=nih, inning_num=inning, starting_outs=nso, on_base_code=obc, away_score=away_score, home_score=home_score, batter=new_batter, batter_pos=new_batter.position, pitcher=new_pitcher, catcher=get_one_lineup(session, this_play.game, new_pitcher_team, position="C"), is_new_inning=switch_sides, is_tied=away_score == home_score, on_first=on_first, on_second=on_second, on_third=on_third, managerai=this_play.managerai, in_pow=in_pow, re24=get_re24(this_play, runs_scored, new_obc=obc, new_starting_outs=nso), ) this_play.wpa = get_wpa(this_play, new_play) this_play.locked = False this_play.complete = True logger.info(f"this_play: {this_play}") logger.info(f"new_play: {new_play}") session.add(this_play) session.add(new_play) session.commit() session.refresh(new_play) return new_play async def get_lineups_from_sheets( session: Session, sheets, this_game: Game, this_team: Team, lineup_num: int, roster_num: int, ) -> list[Lineup]: logger.info(f"get_lineups_from_sheets - sheets: {sheets}") this_sheet = sheets.open_by_key(this_team.gsheet) logger.info(f"this_sheet: {this_sheet}") r_sheet = this_sheet.worksheet_by_title("My Rosters") logger.info(f"r_sheet: {r_sheet}") logger.info(f"lineup_num: {roster_num}") if lineup_num == 1: row_start = 9 row_end = 17 else: row_start = 18 row_end = 26 logger.info(f"roster_num: {roster_num}") if int(roster_num) == 1: l_range = f"H{row_start}:I{row_end}" elif int(roster_num) == 2: l_range = f"J{row_start}:K{row_end}" else: l_range = f"L{row_start}:M{row_end}" logger.info(f"l_range: {l_range}") raw_cells = r_sheet.range(l_range) logger.info(f"raw_cells: {raw_cells}") try: lineup_cells = [(row[0].value, int(row[1].value)) for row in raw_cells] logger.info(f"lineup_cells: {lineup_cells}") except ValueError as e: logger.error(f"Could not pull roster for {this_team.abbrev}: {e}") log_exception( GoogleSheetsException, "Uh oh. Looks like your lineup might not be saved. I am reading blanks when I try to get the card IDs", ) all_lineups = [] all_pos = [] card_ids = [] for index, row in enumerate(lineup_cells): if "" in row: break if row[0].upper() not in all_pos: all_pos.append(row[0].upper()) else: raise SyntaxError( f"You have more than one {row[0].upper()} in this lineup. Please update and set the lineup again." ) this_card = await get_card_or_none(session, card_id=int(row[1])) if this_card is None: raise LookupError( f"Your {row[0].upper()} has a Card ID of {int(row[1])} and I cannot find that card. Did you sell it by chance? Or maybe you sold a duplicate and the bot sold the one you were using?" ) if this_card.team_id != this_team.id: raise SyntaxError( f"Easy there, champ. Looks like card ID {row[1]} belongs to the {this_card.team.lname}. Try again with only cards you own." ) position = row[0].upper() if position != "DH": player_positions = [ getattr(this_card.player, f"pos_{i}") for i in range(1, 9) if getattr(this_card.player, f"pos_{i}") is not None ] if position not in player_positions: raise PositionNotFoundException( f"**{this_card.player.name_with_desc}** (card {this_card.id}) is listed at **{position}** in your lineup, " f"but can only play {', '.join(player_positions)}. Please fix your lineup in Google Sheets." ) card_id = row[1] card_ids.append(str(card_id)) this_lineup = Lineup( position=row[0].upper(), batting_order=index + 1, game=this_game, team=this_team, player=this_card.player, card=this_card, ) all_lineups.append(this_lineup) legal_data = await legal_check([card_ids], difficulty_name=this_game.league_name) logger.debug(f"legal_data: {legal_data}") if not legal_data["legal"]: raise CardLegalityException( f"The following cards appear to be illegal for this game mode:\n{legal_data['error_string']}" ) if len(all_lineups) != 9: raise Exception( f"I was only able to pull in {len(all_lineups)} batters from Sheets. Please check your saved lineup and try again." ) return all_lineups async def get_full_roster_from_sheets( session: Session, interaction: discord.Interaction, sheets, this_game: Game, this_team: Team, roster_num: int, ) -> list[RosterLink]: """ Commits roster links """ logger.debug(f"get_full_roster_from_sheets - sheets: {sheets}") this_sheet = sheets.open_by_key(this_team.gsheet) logger.debug(f"this_sheet: {this_sheet}") r_sheet = this_sheet.worksheet_by_title("My Rosters") logger.debug(f"r_sheet: {r_sheet}") if roster_num == 1: l_range = "B3:B28" elif roster_num == 2: l_range = "B29:B54" else: l_range = "B55:B80" roster_message = await interaction.channel.send( content="I'm diving into Sheets - wish me luck." ) logger.info(f"l_range: {l_range}") raw_cells = r_sheet.range(l_range) logger.info(f"raw_cells: {raw_cells}") await roster_message.edit( content="Got your roster, now to find these cards in your collection..." ) try: card_ids = [row[0].value for row in raw_cells] logger.info(f"card_ids: {card_ids}") except (ValueError, TypeError) as e: logger.error(f"Could not pull roster for {this_team.abbrev}: {e}") logger.error(f"raw_cells type: {type(raw_cells)} / raw_cells: {raw_cells}") raise GoogleSheetsException( f"Uh oh. I had trouble reading your roster from Google Sheets (range {l_range}). Please make sure your roster is saved properly and all cells contain valid card IDs." ) for x in card_ids: this_card = await get_card_or_none(session, card_id=x) if this_card is None: logger.error( f"Card ID {x} not found while loading roster for team {this_team.abbrev}" ) raise CardNotFoundException( f"Card ID {x} was not found in your collection. Please check your roster sheet and make sure all card IDs are valid." ) session.add(RosterLink(game=this_game, card=this_card, team=this_team)) session.commit() await roster_message.edit( content="Your roster is logged and scouting data is available." ) return session.exec( select(RosterLink).where( RosterLink.game == this_game, RosterLink.team == this_team ) ).all() async def checks_log_interaction( session: Session, interaction: discord.Interaction, command_name: str, lock_play: bool = True, ) -> tuple[Game, Team, Play]: """ Validates interaction permissions and optionally locks the current play for processing. Args: session: Database session interaction: Discord interaction command_name: Name of the command being executed lock_play: If True (default), locks the play. Set to False for read-only commands. IMPORTANT: If lock_play=True, the calling code MUST either: 1. Call complete_play() which releases the lock on success, OR 2. Use exception handling to call release_play_lock() on failure Commits this_play with locked=True (only if lock_play=True) """ logger.info( f"log interaction checks for {interaction.user.name} in channel {interaction.channel.name}" ) await interaction.response.defer(thinking=True) this_game = get_channel_game_or_none(session, interaction.channel_id) if this_game is None: raise GameNotFoundException("I don't see an active game in this channel.") owner_team = await get_team_or_none(session, gm_id=interaction.user.id) if owner_team is None: logger.exception( f"{command_name} command: No team found for GM ID {interaction.user.id}" ) raise TeamNotFoundException("Do I know you? I cannot find your team.") if "gauntlet" in this_game.game_type: gauntlet_abbrev = f"Gauntlet-{owner_team.abbrev}" owner_team = await get_team_or_none(session, team_abbrev=gauntlet_abbrev) if owner_team is None: logger.exception( f"{command_name} command: No gauntlet team found with abbrev {gauntlet_abbrev}" ) raise TeamNotFoundException( "Hm, I was not able to find a gauntlet team for you." ) if owner_team.id not in [this_game.away_team_id, this_game.home_team_id]: if interaction.user.id != 258104532423147520: logger.exception( f"{interaction.user.display_name} tried to run a command in Game {this_game.id} when they aren't a GM in the game." ) raise TeamNotFoundException( "Bruh. Only GMs of the active teams can log plays." ) else: await interaction.channel.send( f"Cal is bypassing the GM check to run the {command_name} command" ) this_play = this_game.current_play_or_none(session) if this_play is None: logger.error( f"{command_name} command: No play found for Game ID {this_game.id} - attempting to initialize play" ) this_play = activate_last_play(session, this_game) if lock_play: # Only check and set lock if this is a write command if this_play.locked: logger.warning( f"{interaction.user.name} attempted {command_name} on locked play {this_play.id} " f"in game {this_game.id}. Rejecting duplicate submission." ) raise PlayLockedException( "This play is already being processed. Please wait for the current action to complete.\n\n" "If this play appears stuck, go call dad." ) this_play.locked = True session.add(this_play) session.commit() session.refresh(this_play) return this_game, owner_team, this_play def log_run_scored( session: Session, runner: Lineup, this_play: Play, is_earned: bool = True ): """ Commits last_ab """ logger.info(f"Logging a run for runner ID {runner.id} in Game {this_play.game.id}") last_ab = get_players_last_pa(session, lineup_member=runner) last_ab.run = 1 logger.info(f"last_ab: {last_ab}") errors = session.exec( select(func.count(Play.id)).where( Play.game == this_play.game, Play.inning_num == last_ab.inning_num, Play.inning_half == last_ab.inning_half, Play.error == 1, Play.complete == True, # noqa: E712 ) ).one() outs = session.exec( select(func.sum(Play.outs)).where( Play.game == this_play.game, Play.inning_num == last_ab.inning_num, Play.inning_half == last_ab.inning_half, Play.complete == True, # noqa: E712 ) ).one() logger.info(f"errors: {errors} / outs: {outs}") if outs is not None: if errors + outs + this_play.error >= 3: logger.info("unearned run") is_earned = False last_ab.e_run = 1 if is_earned else 0 session.add(last_ab) session.commit() return True def create_pinch_runner_entry_play( session: Session, game: Game, current_play: Play, pinch_runner_lineup: Lineup ) -> Play: """ Creates a "pinch runner entry" Play record when a pinch runner substitutes for a player on base. This Play has PA=0, AB=0 so it doesn't affect counting stats, but provides a record to mark when the pinch runner scores. Commits the new Play. Args: session: Database session game: The current game current_play: The current active Play (not yet complete) pinch_runner_lineup: The Lineup record for the pinch runner Returns: The newly created entry Play """ logger.info( f"Creating pinch runner entry Play for {pinch_runner_lineup.player.name_with_desc} in Game {game.id}" ) # Get the next play number max_play_num = session.exec( select(func.max(Play.play_num)).where(Play.game == game) ).one() next_play_num = max_play_num + 1 if max_play_num else 1 # Create the entry Play entry_play = Play( game=game, play_num=next_play_num, batter=pinch_runner_lineup, pitcher=current_play.pitcher, catcher=current_play.catcher, inning_half=current_play.inning_half, inning_num=current_play.inning_num, batting_order=pinch_runner_lineup.batting_order, starting_outs=current_play.starting_outs, away_score=current_play.away_score, home_score=current_play.home_score, on_base_code=current_play.on_base_code, batter_pos=pinch_runner_lineup.position, # This is NOT a plate appearance pa=0, ab=0, run=0, # Will be set to 1 if they score # All other stats default to 0 complete=True, # This "event" is complete is_tied=current_play.is_tied, is_go_ahead=False, is_new_inning=False, managerai_id=current_play.managerai_id, ) session.add(entry_play) session.commit() session.refresh(entry_play) logger.info( f"Created entry Play #{entry_play.play_num} for pinch runner {pinch_runner_lineup.player.name_with_desc}" ) return entry_play def advance_runners( session: Session, this_play: Play, num_bases: int, only_forced: bool = False, earned_bases: int = None, ) -> Play: """ No commits """ logger.info(f"Advancing runners {num_bases} bases in game {this_play.game.id}") if earned_bases is None: earned_bases = num_bases this_play.rbi = 0 er_from = { 3: True if earned_bases >= 1 else False, 2: True if earned_bases >= 2 else False, 1: True if earned_bases >= 3 else False, } if num_bases == 0: if this_play.on_first is not None: this_play.on_first_final = 1 if this_play.on_second_id is not None: this_play.on_second_final = 2 if this_play.on_third_id is not None: this_play.on_third_final = 3 elif only_forced: if not this_play.on_first: if this_play.on_second: this_play.on_second_final = 2 if this_play.on_third: this_play.on_third_final = 3 elif this_play.on_second: if this_play.on_third: if num_bases > 0: this_play.on_third_final = 4 log_run_scored( session, this_play.on_third, this_play, is_earned=er_from[3] ) this_play.rbi += 1 if er_from[3] else 0 if num_bases > 1: this_play.on_second_final = 4 log_run_scored( session, this_play.on_second, this_play, is_earned=er_from[2] ) this_play.rbi += 1 if er_from[2] else 0 elif num_bases == 1: this_play.on_second_final = 3 else: this_play.on_second_final = 2 else: if this_play.on_third: this_play.on_third_final = 3 if num_bases > 2: this_play.on_first_final = 4 log_run_scored(session, this_play.on_first, this_play, is_earned=er_from[1]) this_play.rbi += 1 if er_from[1] else 0 elif num_bases == 2: this_play.on_first_final = 3 elif num_bases == 1: this_play.on_first_final = 2 else: this_play.on_first_final = 1 else: if this_play.on_third: if num_bases > 0: this_play.on_third_final = 4 log_run_scored( session, this_play.on_third, this_play, is_earned=er_from[3] ) this_play.rbi += 1 if er_from[3] else 0 else: this_play.on_third_final = 3 if this_play.on_second: if num_bases > 1: this_play.on_second_final = 4 log_run_scored( session, this_play.on_second, this_play, is_earned=er_from[2] ) this_play.rbi += 1 if er_from[2] else 0 elif num_bases == 1: this_play.on_second_final = 3 else: this_play.on_second_final = 2 if this_play.on_first: if num_bases > 2: this_play.on_first_final = 4 log_run_scored( session, this_play.on_first, this_play, is_earned=er_from[1] ) this_play.rbi += 1 if er_from[1] else 0 elif num_bases == 2: this_play.on_first_final = 3 elif num_bases == 1: this_play.on_first_final = 2 else: this_play.on_first_final = 1 return this_play async def show_outfield_cards( session: Session, interaction: discord.Interaction, this_play: Play ) -> Lineup: lf = get_one_lineup( session, this_game=this_play.game, this_team=this_play.pitcher.team, position="LF", ) cf = get_one_lineup( session, this_game=this_play.game, this_team=this_play.pitcher.team, position="CF", ) rf = get_one_lineup( session, this_game=this_play.game, this_team=this_play.pitcher.team, position="RF", ) this_team = this_play.pitcher.team logger.debug( f"lf: {lf.player.name_with_desc}\n\ncf: {cf.player.name_with_desc}\n\nrf: {rf.player.name_with_desc}\n\nteam: {this_team.lname}" ) view = Pagination([interaction.user], timeout=10) view.left_button.label = "Left Fielder" view.left_button.style = discord.ButtonStyle.secondary lf_embed = image_embed( image_url=lf.player.image, title=f"{this_team.sname} LF", color=this_team.color, desc=lf.player.name, author_name=this_team.lname, author_icon=this_team.logo, ) view.cancel_button.label = "Center Fielder" view.cancel_button.style = discord.ButtonStyle.blurple cf_embed = image_embed( image_url=cf.player.image, title=f"{this_team.sname} CF", color=this_team.color, desc=cf.player.name, author_name=this_team.lname, author_icon=this_team.logo, ) view.right_button.label = "Right Fielder" view.right_button.style = discord.ButtonStyle.secondary rf_embed = image_embed( image_url=rf.player.image, title=f"{this_team.sname} RF", color=this_team.color, desc=rf.player.name, author_name=this_team.lname, author_icon=this_team.logo, ) page_num = 1 embeds = [lf_embed, cf_embed, rf_embed] msg = await interaction.channel.send(embed=embeds[page_num], view=view) await view.wait() if view.value: if view.value == "left": page_num = 0 if view.value == "cancel": page_num = 1 if view.value == "right": page_num = 2 else: await msg.edit(content=None, embed=embeds[page_num], view=None) view.value = None if page_num == 0: view.left_button.style = discord.ButtonStyle.blurple view.cancel_button.style = discord.ButtonStyle.secondary view.right_button.style = discord.ButtonStyle.secondary if page_num == 1: view.left_button.style = discord.ButtonStyle.secondary view.cancel_button.style = discord.ButtonStyle.blurple view.right_button.style = discord.ButtonStyle.secondary if page_num == 2: view.left_button.style = discord.ButtonStyle.secondary view.cancel_button.style = discord.ButtonStyle.secondary view.right_button.style = discord.ButtonStyle.blurple view.left_button.disabled = True view.cancel_button.disabled = True view.right_button.disabled = True await msg.edit(content=None, embed=embeds[page_num], view=view) return [lf, cf, rf][page_num] async def flyballs( session: Session, interaction: discord.Interaction, this_play: Play, flyball_type: Literal["a", "ballpark", "b", "b?", "c"], ) -> Play: """ Commits this_play """ this_game = this_play.game num_outs = 1 if flyball_type == "a": this_play.pa, this_play.ab, this_play.outs = 1, 1, 1 if this_play.starting_outs < 2: this_play = advance_runners(session, this_play, num_bases=1) if this_play.on_third: this_play.ab = 0 elif flyball_type == "b" or flyball_type == "ballpark": this_play.pa, this_play.ab, this_play.outs = 1, 1, 1 this_play.bpfo = 1 if flyball_type == "ballpark" else 0 this_play = advance_runners(session, this_play, num_bases=0) if this_play.starting_outs < 2 and this_play.on_third: this_play.ab = 0 this_play.rbi = 1 this_play.on_third_final = 4 log_run_scored(session, this_play.on_third, this_play) if this_play.starting_outs < 2 and this_play.on_second: logger.debug("calling of embed") this_of = await show_outfield_cards(session, interaction, this_play) of_rating = await get_position(session, this_of.card, this_of.position) of_mod = 0 if this_of.position == "LF": of_mod = -2 elif this_of.position == "RF": of_mod = 2 logger.debug("done with of embed") runner_lineup = this_play.on_second runner = runner_lineup.player max_safe = ( runner_lineup.card.batterscouting.battingcard.running + of_rating.arm + of_mod ) min_out = 20 + of_rating.arm + of_mod if (min_out >= 20 and max_safe >= 20) or min_out > 20: min_out = 21 min_hold = max_safe + 1 safe_string = f"1{' - ' if max_safe > 1 else ''}" if max_safe > 1: if max_safe <= 20: safe_string += f"{max_safe}" else: safe_string += "20" if min_out > 20: out_string = "None" else: out_string = f"{min_out}{' - 20' if min_out < 20 else ''}" hold_string = "" if max_safe != min_out: hold_string += f"{min_hold}" if min_out - 1 > min_hold: hold_string += f" - {min_out - 1}" ranges_embed = this_of.team.embed ranges_embed.title = "Tag Play" ranges_embed.description = f"{this_of.team.abbrev} {this_of.position} {this_of.card.player.name}'s Throw vs {runner.name}" ranges_embed.add_field( name="Runner Speed", value=runner_lineup.card.batterscouting.battingcard.running, ) ranges_embed.add_field( name=f"{this_of.position} Arm", value=f"{'+' if of_rating.arm > 0 else ''}{of_rating.arm}", ) ranges_embed.add_field(name=f"{this_of.position} Mod", value=f"{of_mod}") ranges_embed.add_field(name="", value="", inline=False) ranges_embed.add_field(name="Safe Range", value=safe_string) ranges_embed.add_field(name="Hold Range", value=hold_string) ranges_embed.add_field(name="Out Range", value=out_string) await interaction.channel.send(content=None, embed=ranges_embed) if this_play.ai_is_batting: tag_resp = this_play.managerai.tag_from_second(session, this_game) logger.info(f"tag_resp: {tag_resp}") tagging_from_second = tag_resp.min_safe >= max_safe if tagging_from_second: await interaction.channel.send( content=f"**{runner.name}** is tagging from second!" ) else: await interaction.channel.send( content=f"**{runner.name}** is holding at second." ) else: tagging_from_second = await ask_confirm( interaction, question=f"Is {runner.name} attempting to tag up from second?", label_type="yes", ) if tagging_from_second: this_roll = d_twenty_roll(this_play.pitcher.team, this_play.game) if min_out is not None and this_roll.d_twenty >= min_out: result = "out" elif this_roll.d_twenty <= max_safe: result = "safe" else: result = "holds" await interaction.channel.send(content=None, embeds=this_roll.embeds) is_correct = await ask_confirm( interaction, question=f"Looks like {runner.name} {'is' if result != 'holds' else ''} **{result.upper()}** at {'third' if result != 'holds' else 'second'}! Is that correct?", label_type="yes", delete_question=False, ) if not is_correct: view = ButtonOptions( responders=[interaction.user], timeout=60, labels=["Safe at 3rd", "Hold at 2nd", "Out at 3rd", None, None], ) question = await interaction.channel.send( f"What was the result of {runner.name} tagging from second?", view=view, ) await view.wait() if view.value: await question.delete() if view.value == "Tagged Up": result = "safe" elif view.value == "Out at 3rd": result = "out" else: result = "holds" else: await question.delete() if result == "safe": this_play.on_second_final = 3 elif result == "out": num_outs += 1 this_play.on_second_final = None this_play.outs = num_outs elif flyball_type == "b?": this_play.pa, this_play.ab, this_play.outs = 1, 1, 1 this_play = advance_runners(session, this_play, 0) if this_play.starting_outs < 2 and this_play.on_third: logger.debug("calling of embed") this_of = await show_outfield_cards(session, interaction, this_play) of_rating = await get_position(session, this_of.card, this_of.position) logger.debug("done with of embed") runner_lineup = this_play.on_third runner = runner_lineup.player max_safe = ( runner_lineup.card.batterscouting.battingcard.running + of_rating.arm ) safe_string = f"1{' - ' if max_safe > 1 else ''}" if max_safe > 1: if max_safe <= 20: safe_string += f"{max_safe - 1}" else: safe_string += "20" if max_safe == 20: out_string = "None" catcher_string = "20" elif max_safe > 20: out_string = "None" catcher_string = "None" elif max_safe == 19: out_string = "None" catcher_string = "19 - 20" elif max_safe == 18: out_string = "20" catcher_string = "18 - 19" else: out_string = f"{max_safe + 2} - 20" catcher_string = f"{max_safe} - {max_safe + 1}" true_max_safe = max_safe - 1 true_min_out = max_safe + 2 ranges_embed = this_play.batter.team.embed ranges_embed.title = "Play at the Plate" ranges_embed.description = ( f"{runner.name} vs {this_of.card.player.name}'s Throw" ) ranges_embed.add_field( name=f"{this_of.position} Arm", value=f"{'+' if of_rating.arm > 0 else ''}{of_rating.arm}", ) ranges_embed.add_field( name="Runner Speed", value=runner_lineup.card.batterscouting.battingcard.running, ) ranges_embed.add_field(name="", value="", inline=False) ranges_embed.add_field(name="Safe Range", value=safe_string) ranges_embed.add_field(name="Catcher Check", value=catcher_string) ranges_embed.add_field(name="Out Range", value=out_string) await interaction.channel.send(content=None, embed=ranges_embed) if this_play.ai_is_batting: tag_resp = this_play.managerai.tag_from_third(session, this_game) logger.info(f"tag_resp: {tag_resp}") tagging_from_third = tag_resp.min_safe <= max_safe if tagging_from_third: await interaction.channel.send( content=f"**{runner.name}** is tagging from third!" ) else: await interaction.channel.send( content=f"**{runner.name}** is holding at third." ) else: tagging_from_third = await ask_confirm( interaction, question=f"Is {runner.name} attempting to tag up from third?", label_type="yes", ) if tagging_from_third: this_roll = d_twenty_roll(this_play.batter.team, this_play.game) if this_roll.d_twenty <= true_max_safe: result = "safe" q_text = f"Looks like {runner.name} is SAFE at home!" out_at_home = False elif this_roll.d_twenty >= true_min_out: result = "out" q_text = f"Looks like {runner.name} is OUT at home!" out_at_home = True else: result = "catcher" q_text = f"Looks like this is a check for {this_play.catcher.player.name} to block the plate!" await interaction.channel.send(content=None, embeds=this_roll.embeds) is_correct = await ask_confirm( interaction, question=f"{q_text} Is that correct?", label_type="yes", delete_question=False, ) if not is_correct: out_at_home = await ask_confirm( interaction, question=f"Was {runner.name} thrown out?", label_type="yes", ) elif result == "catcher": catcher_rating = await get_position( session, this_play.catcher.card, "C" ) this_roll = d_twenty_roll(this_play.catcher.team, this_play.game) runner_embed = this_play.batter.team.embed runner_embed.title = f"{this_play.on_third.player.name} To Home" runner_embed.description = f"{this_play.catcher.team.abbrev} C {this_play.catcher.player.name} Blocking the Plate" runner_embed.add_field( name="Catcher Range", value=catcher_rating.range ) runner_embed.add_field(name="", value="", inline=False) if catcher_rating.range == 1: safe_range = 3 elif catcher_rating.range == 2: safe_range = 7 elif catcher_rating.range == 3: safe_range = 11 elif catcher_rating.range == 4: safe_range = 15 elif catcher_rating.range == 5: safe_range = 19 runner_embed.add_field(name="Safe Range", value=f"1 - {safe_range}") out_range = f"{safe_range + 1}" if safe_range < 19: out_range += " - 20" runner_embed.add_field(name="Out Range", value=out_range) await interaction.channel.send(content=None, embed=runner_embed) await interaction.channel.send( content=None, embeds=this_roll.embeds ) if this_roll.d_twenty <= safe_range: logger.info( f"Roll of {this_roll.d_twenty} is SAFE {AT_BASE[4]}" ) out_at_home = False q_text = f"Looks like **{runner.name}** is SAFE {AT_BASE[4]}!" else: logger.info(f"Roll of {this_roll.d_twenty} is OUT {AT_BASE[4]}") out_at_home = True q_text = f"Looks like **{runner.name}** is OUT {AT_BASE[4]}!" is_correct = await ask_confirm( interaction=interaction, question=f"{q_text} Is that correct?", label_type="yes", ) if not is_correct: logger.info( f"{interaction.user.name} says this result is incorrect - setting out_at_home to {not out_at_home}" ) out_at_home = not out_at_home if out_at_home: num_outs += 1 this_play.on_third_final = None this_play.outs = num_outs else: this_play.ab = 0 this_play.rbi = 1 this_play.on_third_final = 4 log_run_scored(session, this_play.on_third, this_play) elif flyball_type == "c": this_play.pa, this_play.ab, this_play.outs = 1, 1, 1 this_play = advance_runners(session, this_play, num_bases=0) session.add(this_play) session.commit() session.refresh(this_play) return this_play async def lineouts( session: Session, interaction: discord.Interaction, this_play: Play, lineout_type: Literal["one-out", "ballpark", "max-outs"], ) -> Play: """ Commits this_play """ num_outs = 1 this_play.pa, this_play.ab, this_play.outs = 1, 1, 1 this_play.bplo = 1 if lineout_type == "ballpark" else 0 this_play = advance_runners(session, this_play, num_bases=0) if ( lineout_type == "max-outs" and this_play.on_base_code > 0 and this_play.starting_outs < 2 ): logger.info("Lomax going in") if this_play.on_base_code <= 3 or this_play.starting_outs == 1: logger.info("Lead runner is out") this_play.outs = 2 if this_play.on_third is not None: this_play.on_third_final = None elif this_play.on_second is not None: this_play.on_second_final = None elif this_play.on_first is not None: this_play.on_first_final = None else: logger.info("Potential triple play") this_roll = d_twenty_roll(this_play.pitcher.team, this_play.game) ranges_embed = this_play.pitcher.team.embed ranges_embed.title = "Potential Triple Play" ranges_embed.description = f"{this_play.pitcher.team.lname}" ranges_embed.add_field(name="Double Play Range", value="1 - 13") ranges_embed.add_field(name="Triple Play Range", value="14 - 20") await interaction.edit_original_response( content=None, embeds=[ranges_embed, *this_roll.embeds] ) if this_roll.d_twenty > 13: logger.info(f"Roll of {this_roll.d_twenty} is a triple play!") num_outs = 3 else: logger.info(f"Roll of {this_roll.d_twenty} is a double play!") num_outs = 2 is_correct = await ask_confirm( interaction, question=f"Looks like this is a {'triple' if num_outs == 3 else 'double'} play! Is that correct?", ) if not is_correct: logger.warning(f"{interaction.user.name} marked this result incorrect") num_outs = 2 if num_outs == 3 else 3 if num_outs == 2: logger.info("Lead baserunner is out") this_play.outs = 2 out_marked = False if this_play.on_third is not None: this_play.on_third_final = None out_marked = True elif this_play.on_second and not out_marked: this_play.on_second_final = None out_marked = True elif this_play.on_first and not out_marked: this_play.on_first_final = None out_marked = True else: logger.info("Two baserunners are out") this_play.outs = 3 outs_marked = 1 if this_play.on_third is not None: this_play.on_third_final = None outs_marked += 1 elif this_play.on_second is not None: this_play.on_second_final = None outs_marked += 1 elif this_play.on_first is not None and outs_marked < 3: this_play.on_first_final = None outs_marked += 1 session.add(this_play) session.commit() session.refresh(this_play) return this_play async def frame_checks( session: Session, interaction: discord.Interaction, this_play: Play ): """ Commits this_play """ this_roll = frame_plate_check(this_play.batter.team, this_play.game) logger.info(f"this_roll: {this_roll}") await interaction.edit_original_response(content=None, embeds=this_roll.embeds) if this_roll.is_walk: this_play = await walks(session, interaction, this_play, "unintentional") else: this_play = await strikeouts(session, interaction, this_play) session.add(this_play) session.commit() await asyncio.sleep(1.5) session.refresh(this_play) return this_play @log_errors async def check_uncapped_advance( session: Session, interaction: discord.Interaction, this_play: Play, lead_runner: Lineup, lead_base: int, trail_runner: Lineup, trail_base: int, ): this_game = this_play.game outfielder = await show_outfield_cards(session, interaction, this_play) logger.info(f"throw from {outfielder.player.name_with_desc}") this_roll = d_twenty_roll(this_play.batter.team, this_play.game) block_roll = d_twenty_roll(this_play.catcher.team, this_play.game) def_team = this_play.pitcher.team get_batter_card(this_lineup=lead_runner) of_rating = await get_position( session, this_card=outfielder.card, position=outfielder.position ) c_rating = await get_position(session, this_play.catcher.card, position="C") runner_embed = this_play.batter.team.embed def_alignment = this_play.managerai.defense_alignment(session, this_play.game) lead_bc = get_batter_card(this_lineup=lead_runner) logger.info(f"lead runner batting card: {lead_bc}") lead_safe_range = lead_bc.running + of_rating.arm logger.info(f"lead_safe_range: {lead_safe_range}") # Build lead runner embed lead_runner_embed = copy.deepcopy(runner_embed) lead_runner_embed.title = ( f"{lead_runner.player.name} To {'Home' if lead_base == 4 else 'Third'}" ) lead_runner_embed.description = f"{outfielder.team.abbrev} {outfielder.position} {outfielder.player.name}'s Throw" lead_runner_embed.add_field( name="Runner Speed", value=lead_runner.card.batterscouting.battingcard.running ) lead_runner_embed.add_field( name=f"{outfielder.position} Arm", value=f"{'+' if of_rating.arm > 0 else ''}{of_rating.arm}", ) if this_play.starting_outs == 2: logger.info("Adding 2 for 2 outs") lead_safe_range += 2 lead_runner_embed.add_field(name="2-Out Mod", value="+2") if lead_base == 3 and outfielder.position != "CF": of_mod = -2 if outfielder.position == "LF" else 2 logger.info(f"{outfielder.position} to 3B mod: {of_mod}") lead_safe_range += of_mod lead_runner_embed.add_field( name=f"{outfielder.position} Mod", value=f"{'+' if of_mod > 0 else ''}{of_mod}", ) logger.info(f"lead_runner_embed: {lead_runner_embed}") # Build trail runner embed trail_runner_embed = copy.deepcopy(runner_embed) trail_bc = get_batter_card(this_lineup=trail_runner) logger.info(f"trail runner batting card: {trail_bc}") trail_runner_embed.title = ( f"{trail_runner.player.name} To {'Third' if trail_base == 3 else 'Second'}" ) trail_runner_embed.description = f"{outfielder.team.abbrev} {outfielder.position} {outfielder.player.name}'s Throw" trail_runner_embed.add_field(name="Runner Speed", value=trail_bc.running) trail_runner_embed.add_field( name=f"{outfielder.position} Arm", value=f"{'+' if of_rating.arm > 0 else ''}{of_rating.arm}", ) trail_safe_range = trail_bc.running - 5 + of_rating.arm logger.info(f"trail_safe_range: {trail_safe_range}") if trail_base == 3 and outfielder.position != "CF": of_mod = -2 if outfielder.position == "LF" else 2 logger.info(f"{outfielder.position} to 3B mod: {of_mod}") trail_safe_range += of_mod trail_runner_embed.add_field( name=f"{outfielder.position} Mod", value=f"{'+' if of_mod > 0 else ''}{of_mod}", inline=False, ) trail_runner_embed.add_field(name="Trail Runner", value="-5") def at_home_strings(safe_range: int): safe_string = f"1{' - ' if safe_range > 1 else ''}" if safe_range > 1: if safe_range <= 20: safe_string += f"{safe_range - 1}" else: safe_string += "20" if safe_range == 20: out_string = "None" catcher_string = "20" elif safe_range > 20: out_string = "None" catcher_string = "None" elif safe_range == 19: out_string = "None" catcher_string = "19 - 20" elif safe_range == 18: out_string = "20" catcher_string = "18 - 19" else: out_string = f"{safe_range + 2} - 20" catcher_string = f"{safe_range} - {safe_range + 1}" logger.info( f"safe: {safe_string} / catcher: {catcher_string} / out: {out_string}" ) return {"safe": safe_string, "catcher": catcher_string, "out": out_string} def at_third_strings(safe_range: int): safe_string = f"1{' - ' if safe_range > 1 else ''}" if safe_range > 1: if safe_range <= 20: safe_string += f"{safe_range}" else: safe_string += "20" if safe_range > 19: out_string = "20" else: out_string = f"{safe_range + 1} - 20" logger.info(f"safe: {safe_string} / out: {out_string}") return {"safe": safe_string, "out": out_string} async def out_at_home(safe_range: int): if this_roll.d_twenty in [safe_range, safe_range + 1]: logger.info( f"Roll of {this_roll.d_twenty} is a catcher check with safe range of {safe_range}" ) is_block_plate = await ask_confirm( interaction, question=f"Looks like **{this_play.catcher.player.name}** has a chance to block the plate! Is that correct?", label_type="yes", delete_question=False, ) if is_block_plate: logger.info("Looks like a block the plate check") await interaction.channel.send(content=None, embeds=block_roll.embeds) if block_roll.d_twenty > RANGE_CHECKS[c_rating.range]: logger.info(f"Roll of {this_roll.d_twenty} is OUT {AT_BASE[4]}") runner_thrown_out = True q_text = ( f"Looks like **{lead_runner.player.name}** is OUT {AT_BASE[4]}!" ) else: logger.info(f"Roll of {this_roll.d_twenty} is SAFE {AT_BASE[4]}") runner_thrown_out = False q_text = f"Looks like **{lead_runner.player.name}** is SAFE {AT_BASE[4]}!" is_correct = await ask_confirm( interaction=interaction, question=f"{q_text} Is that correct?", label_type="yes", ) if not is_correct: logger.info( f"{interaction.user.name} says this result is incorrect - setting runner_thrown_out to {not runner_thrown_out}" ) runner_thrown_out = not runner_thrown_out else: runner_thrown_out = await ask_confirm( interaction=interaction, question=f"Was **{lead_runner.player.name}** thrown out {AT_BASE[4]}?", label_type="yes", ) else: logger.info( f"Roll of {this_roll.d_twenty} has a clear result with safe range of {safe_range}" ) if this_roll.d_twenty > safe_range: logger.info(f"Roll of {this_roll.d_twenty} is OUT {AT_BASE[4]}") runner_thrown_out = True q_text = ( f"Looks like **{lead_runner.player.name}** is OUT {AT_BASE[4]}!" ) else: logger.info(f"Roll of {this_roll.d_twenty} is SAFE {AT_BASE[4]}") runner_thrown_out = False q_text = ( f"Looks like **{lead_runner.player.name}** is SAFE {AT_BASE[4]}!" ) is_correct = await ask_confirm( interaction, question=f"{q_text} Is that correct?", label_type="yes", delete_question=False, ) if not is_correct: logger.warning( f"{interaction.user.name} says call is incorrect; runner is {'not ' if runner_thrown_out else ''}thrown out" ) runner_thrown_out = not runner_thrown_out return runner_thrown_out async def out_at_base(safe_range: int, this_runner: Lineup, this_base: int): if this_roll.d_twenty > safe_range: logger.info(f"Roll of {this_roll.d_twenty} is OUT {AT_BASE[this_base]}") runner_thrown_out = True q_text = ( f"Looks like **{this_runner.player.name}** is OUT {AT_BASE[this_base]}!" ) else: logger.info(f"Roll of {this_roll.d_twenty} is SAFE {AT_BASE[this_base]}") runner_thrown_out = False q_text = f"Looks like **{this_runner.player.name}** is SAFE {AT_BASE[this_base]}!" is_correct = await ask_confirm( interaction, question=f"{q_text} Is that correct?", label_type="yes", delete_question=False, ) if not is_correct: logger.warning( f"{interaction.user.name} says call is incorrect; runner is {'not ' if runner_thrown_out else ''}thrown out" ) runner_thrown_out = not runner_thrown_out return runner_thrown_out # Either there is no AI team or the AI is pitching if not this_game.ai_team or not this_play.ai_is_batting: # Build lead runner embed # Check for lead runner hold if (lead_runner == this_play.on_second and def_alignment.hold_second) or ( lead_runner == this_play.on_first and def_alignment.hold_first ): lead_safe_range -= 1 logger.info(f"Lead runner was held, -1 to safe range: {lead_safe_range}") lead_runner_embed.add_field(name="Runner Held", value="-1") else: logger.info( f"Lead runner was not held, +1 to safe range: {lead_safe_range}" ) lead_safe_range += 1 lead_runner_embed.add_field(name="Runner Not Held", value="+1") lead_runner_embed.add_field(name="", value="", inline=False) if lead_base == 4: logger.info("lead base is 4, building strings") lead_strings = at_home_strings(lead_safe_range) lead_runner_embed.add_field(name="Safe Range", value=lead_strings["safe"]) lead_runner_embed.add_field( name="Catcher Check", value=lead_strings["catcher"] ) lead_runner_embed.add_field(name="Out Range", value=lead_strings["out"]) else: logger.info("lead base is 3, building strings") lead_strings = at_third_strings(lead_safe_range) lead_runner_embed.add_field(name="Safe Range", value=lead_strings["safe"]) lead_runner_embed.add_field(name="Out Range", value=lead_strings["out"]) # Build trail runner embed if trail_runner == this_play.on_first and def_alignment.hold_first: trail_safe_range -= 1 logger.info(f"Trail runner was held, -1 to safe range: {trail_safe_range}") trail_runner_embed.add_field(name="Runner Held", value="-1") elif trail_runner == this_play.on_first and not def_alignment.hold_first: trail_safe_range += 1 logger.info( f"Trail runner was not held, +1 to safe range: {trail_safe_range}" ) trail_runner_embed.add_field(name="Runner Not Held", value="+1") else: logger.info("Trail runner was not from first base, no hold modifier") trail_runner_embed.add_field(name="", value="", inline=False) logger.info("Building strings for trail runner") safe_string = f"1{' - ' if trail_safe_range > 1 else ''}" if trail_safe_range > 1: if trail_safe_range < 20: safe_string += f"{trail_safe_range}" else: logger.info("capping safe range at 19") trail_safe_range = 19 safe_string += "19" out_string = f"{trail_safe_range + 1} - 20" logger.info(f"safe: {safe_string} / out: {out_string}") trail_runner_embed.add_field(name="Safe Range", value=safe_string) trail_runner_embed.add_field(name="Out Range", value=out_string) await interaction.channel.send(embeds=[lead_runner_embed, trail_runner_embed]) is_lead_running = await ask_confirm( interaction=interaction, question=f"Is **{lead_runner.player.name}** being sent {TO_BASE[lead_base]}?", label_type="yes", ) if is_lead_running: throw_resp = None if this_game.ai_team: throw_resp = this_play.managerai.throw_at_uncapped(session, this_game) logger.info(f"throw_resp: {throw_resp}") if throw_resp.cutoff: await interaction.channel.send( f"The {def_team.sname} will cut off the throw {TO_BASE[lead_base]}" ) if this_play.on_second == lead_runner: this_play.rbi += 1 this_play.on_second_final = 4 log_run_scored(session, lead_runner, this_play) else: this_play.on_first_final = 3 await asyncio.sleep(1) return this_play else: await interaction.channel.send( content=f"**{outfielder.player.name}** is throwing {TO_BASE[lead_base]}!" ) else: throw_for_lead = await ask_confirm( interaction=interaction, question=f"Is the defense throwing {TO_BASE[lead_base]} for {lead_runner.player.name}?", label_type="yes", ) # Human defense is cutting off the throw if not throw_for_lead: await question.delete() if this_play.on_second == lead_runner: this_play.rbi += 1 this_play.on_second_final = 4 log_run_scored(session, lead_runner, this_play) return this_play # Human runner is advancing, defense is throwing trail_advancing = await ask_confirm( interaction=interaction, question=f"Is **{trail_runner.player.name}** being sent {TO_BASE[trail_base]} as the trail runner?", label_type="yes", ) # Trail runner is advancing if trail_advancing: throw_lead = False if this_game.ai_team: if ( throw_resp.at_trail_runner and trail_safe_range <= throw_resp.trail_max_safe and trail_safe_range <= throw_resp.trail_max_safe_delta - lead_safe_range ): logger.info( f"defense throwing at trail runner {AT_BASE[trail_base]}" ) await interaction.channel.send( f"**{outfielder.player.name}** will throw {TO_BASE[trail_base]}!" ) throw_lead = False else: logger.info( f"defense throwing at lead runner {AT_BASE[lead_base]}" ) await interaction.channel.send( f"**{outfielder.player.name}** will throw {TO_BASE[lead_base]}!" ) throw_lead = True else: view = Confirm( responders=[interaction.user], timeout=60, label_type="yes" ) view.confirm.label = ( "Home Plate" if lead_base == 4 else "Third Base" ) view.cancel.label = ( "Third Base" if trail_base == 3 else "Second Base" ) question = await interaction.channel.send( f"Is the throw going {TO_BASE[lead_base]} or {TO_BASE[trail_base]}?", view=view, ) throw_lead = await view.wait() # Throw is going to lead runner if throw_lead: logger.info("Throw is going to lead base") try: await question.delete() except (discord.NotFound, UnboundLocalError): pass if this_play.on_first == trail_runner: this_play.on_first_final += 1 elif this_play.batter == trail_runner: this_play.batter_final += 1 else: log_exception( LineupsMissingException, "Could not find trail runner to advance", ) # Throw is going to trail runner else: try: await question.delete() except (discord.NotFound, UnboundLocalError): pass await interaction.channel.send( content=None, embeds=this_roll.embeds ) runner_thrown_out = await out_at_base( trail_safe_range, trail_runner, trail_base ) # Trail runner is thrown out if runner_thrown_out: logger.info("logging one one additional out for trail runner") # Log out on play this_play.outs += 1 # Remove trail runner if this_play.on_first == trail_runner: this_play.on_first_final = None else: this_play.batter_final = None else: logger.info("Runner is safe") if this_play.on_first == trail_runner: this_play.on_first_final += 1 if this_play.on_first_final == 4: logger.info("Add an rbi") this_play.rbi += 1 elif this_play.batter == trail_runner: this_play.batter_final += 1 else: log_exception( LineupsMissingException, "Could not find trail runner to advance", ) # Advance lead runner extra base logger.info("advancing lead runner") if this_play.on_second == lead_runner: logger.info("run scored from second") this_play.rbi += 1 this_play.on_second_final = 4 log_run_scored(session, lead_runner, this_play) elif this_play.on_first == lead_runner: this_play.on_first_final += 1 if this_play.on_first_final > 3: logger.info("run scored from first") this_play.rbi += 1 log_run_scored(session, lead_runner, this_play) if trail_runner != this_play.batter: logger.info("Trail runner is not batter, advancing batter") this_play.batter_final += 1 return this_play # Ball is going to lead base, ask if safe await interaction.channel.send(content=None, embeds=this_roll.embeds) runner_thrown_out = ( await out_at_home(lead_safe_range) if lead_base == 4 else await out_at_base(lead_safe_range, lead_runner, lead_base) ) # Lead runner is thrown out if runner_thrown_out: logger.info("Lead runner is thrown out.") this_play.outs += 1 # Lead runner is safe else: logger.info("Lead runner is safe.") if this_play.on_second == lead_runner: logger.info("setting lead runner on_second_final") if runner_thrown_out: this_play.on_second_final = None else: this_play.on_second_final = lead_base if lead_base == 4: log_run_scored(session, this_play.on_second, this_play) elif this_play.on_first == lead_runner: logger.info("setting lead runner on_first") if runner_thrown_out: this_play.on_first_final = None else: this_play.on_first_final = lead_base if lead_base == 4: log_run_scored(session, this_play.on_first, this_play) else: log_exception( LineupsMissingException, "Could not find lead runner to set final destination", ) # Human lead runner is not advancing else: return this_play elif this_play.ai_is_batting: run_resp = this_play.managerai.uncapped_advance( session, this_game, lead_base, trail_base ) lead_runner_held = await ask_confirm( interaction=interaction, question=f"Was **{lead_runner.player.name}** held at {'second' if lead_runner == this_play.on_second else 'first'} before the pitch?", label_type="yes", ) if lead_runner_held: lead_safe_range -= 1 lead_runner_embed.add_field(name="Runner Held", value="-1") logger.info(f"runner was held, -1 to lead safe range: {lead_safe_range}") else: lead_safe_range += 1 lead_runner_embed.add_field(name="Runner Not Held", value="+1") logger.info( f"runner was not held, +1 to lead safe range: {lead_safe_range}" ) if lead_safe_range < run_resp.min_safe: logger.info("AI is not advancing with lead runner") return this_play logger.info("Building embeds") lead_runner_embed.add_field(name="", value="", inline=False) if lead_base == 4: logger.info("lead base is 4, building strings") lead_strings = at_home_strings(lead_safe_range) lead_runner_embed.add_field(name="Safe Range", value=lead_strings["safe"]) lead_runner_embed.add_field( name="Catcher Check", value=lead_strings["catcher"] ) lead_runner_embed.add_field(name="Out Range", value=lead_strings["out"]) else: logger.info("lead base is 3, building strings") lead_strings = at_third_strings(lead_safe_range) lead_runner_embed.add_field(name="Safe Range", value=lead_strings["safe"]) lead_runner_embed.add_field(name="Out Range", value=lead_strings["out"]) if trail_runner == this_play.on_first: trail_runner_held = await ask_confirm( interaction=interaction, question=f"Was **{trail_runner.player.name}** held at first before the pitch?", label_type="yes", ) logger.info(f"Trail runner held: {trail_runner_held}") if trail_runner_held: trail_runner_embed.add_field(name="Runner Held", value="-1") trail_safe_range -= 1 logger.info(f"Trail runner held, -1 to safe range: {trail_safe_range}") else: trail_runner_embed.add_field(name="Runner Not Held", value="+1") trail_safe_range += 1 logger.info( f"Trail runner not held, +1 to safe range: {trail_safe_range}" ) trail_strings = at_third_strings(trail_safe_range) trail_runner_embed.add_field(name="", value="", inline=False) trail_runner_embed.add_field(name="Safe Range", value=trail_strings["safe"]) trail_runner_embed.add_field(name="Out Range", value=trail_strings["out"]) await interaction.channel.send(embeds=[lead_runner_embed, trail_runner_embed]) is_defense_throwing = await ask_confirm( interaction=interaction, question=f"{lead_runner.player.name} is advancing {TO_BASE[lead_base]} with a safe range of **1->{lead_safe_range if lead_base == 3 else lead_safe_range - 1}**! Is the defense throwing?", label_type="yes", ) # Human defense is not throwing for lead runner if not is_defense_throwing: logger.info("Defense is not throwing for lead runner") if this_play.on_second == lead_runner: this_play.rbi += 1 this_play.on_second_final = 4 log_run_scored(session, lead_runner, this_play) elif this_play.on_first == lead_runner: if this_play.double: this_play.rbi += 1 this_play.on_first_final = 4 log_run_scored(session, lead_runner, this_play) else: this_play.on_first_final = 3 return this_play # Human throw is not being cut off if run_resp.send_trail: await interaction.channel.send( f"**{trail_runner.player.name}** is advancing {TO_BASE[trail_base]} as the trail runner!", ) is_throwing_lead = await ask_confirm( interaction=interaction, question=f"Is the throw going {TO_BASE[lead_base]} or {TO_BASE[trail_base]}?", label_type="yes", custom_confirm_label="Home Plate" if lead_base == 4 else "Third Base", custom_cancel_label="Third Base" if trail_base == 3 else "Second Base", ) # Trail runner advances, throwing for lead runner if is_throwing_lead: if this_play.on_first == trail_runner: this_play.on_first_final += 1 elif this_play.batter == trail_runner: this_play.batter_final += 1 else: log_exception( LineupsMissingException, "Could not find trail runner to advance", ) # Throw is going to trail runner else: runner_thrown_out = await out_at_base( trail_safe_range, trail_runner, trail_base ) if runner_thrown_out: logger.info("Runner was thrown out") # Log out on play this_play.outs += 1 # Remove trail runner logger.info("Remove trail runner") if this_play.on_first == trail_runner: this_play.on_first_final = None else: this_play.batter_final = None else: logger.info("Runner is safe") if this_play.on_first == trail_runner: this_play.on_first_final += 1 if this_play.on_first_final == 4: this_play.rbi += 1 elif this_play.batter == trail_runner: this_play.batter_final += 1 else: log_exception( LineupsMissingException, "Could not find trail runner to advance", ) # Advance lead runner extra base logger.info("Advance lead runner extra base") if this_play.on_second == lead_runner: this_play.rbi += 1 this_play.on_second_final = 4 log_run_scored(session, lead_runner, this_play) elif this_play.on_first == lead_runner: this_play.on_first_final += 1 if this_play.on_first_final > 3: this_play.rbi += 1 log_run_scored(session, lead_runner, this_play) if trail_runner != this_play.batter: logger.info("Trail runner is not batter, advancing batter") this_play.batter_final += 1 return this_play else: await interaction.channel.send( content=f"**{trail_runner.player.name}** is NOT trailing to {TO_BASE[trail_base]}." ) # Ball is going to lead base, ask if safe logger.info("Throw is going to lead base") await interaction.channel.send(content=None, embeds=this_roll.embeds) runner_thrown_out = ( await out_at_home(lead_safe_range) if lead_base == 4 else await out_at_base(lead_safe_range, trail_runner, trail_base) ) # runner_thrown_out = await ask_confirm( # interaction=interaction, # question=f'Was **{lead_runner.player.name}** thrown out {AT_BASE[lead_base]}?', # label_type='yes', # ) # Lead runner is thrown out if runner_thrown_out: logger.info("Lead runner is thrown out.") this_play.outs += 1 if this_play.on_second == lead_runner: logger.info("setting lead runner on_second_final") if runner_thrown_out: this_play.on_second_final = None else: this_play.on_second_final = lead_base if lead_base == 4: log_run_scored(session, this_play.on_second, this_play) elif this_play.on_first == lead_runner: logger.info("setting lead runner on_first_final") if runner_thrown_out: this_play.on_first_final = None else: this_play.on_first_final = lead_base if lead_base == 4: log_run_scored(session, this_play.on_first, this_play) else: log_exception( LineupsMissingException, "Could not find lead runner to set final destination", ) return this_play async def singles( session: Session, interaction: discord.Interaction, this_play: Play, single_type: Literal["*", "**", "ballpark", "uncapped"], ) -> Play: """ Commits this_play """ this_play.hit, this_play.batter_final = 1, 1 if single_type == "**": this_play = advance_runners(session, this_play, num_bases=2) elif single_type in ["*", "ballpark"]: this_play = advance_runners(session, this_play, num_bases=1) this_play.bp1b = 1 if single_type == "ballpark" else 0 elif single_type == "uncapped": this_play = advance_runners(session, this_play, 1) if this_play.on_base_code in [1, 2, 4, 5, 6, 7]: if this_play.on_second: lead_runner = this_play.on_second lead_base = 4 if this_play.on_first: trail_runner = this_play.on_first trail_base = 3 else: trail_runner = this_play.batter trail_base = 2 else: lead_runner = this_play.on_first lead_base = 3 trail_runner = this_play.batter trail_base = 2 this_play = await check_uncapped_advance( session, interaction, this_play, lead_runner, lead_base, trail_runner, trail_base, ) session.add(this_play) session.commit() session.refresh(this_play) return this_play async def doubles( session: Session, interaction: discord.Interaction, this_play: Play, double_type: Literal["**", "***", "uncapped"], ) -> Play: """ Commits this_play """ this_play.hit, this_play.double, this_play.batter_final = 1, 1, 2 if double_type == "**": this_play = advance_runners(session, this_play, num_bases=2) elif double_type == "***": this_play = advance_runners(session, this_play, num_bases=3) elif double_type == "uncapped": this_play = advance_runners(session, this_play, num_bases=2) if this_play.on_first: this_play = await check_uncapped_advance( session, interaction, this_play, lead_runner=this_play.on_first, lead_base=4, trail_runner=this_play.batter, trail_base=3, ) session.add(this_play) session.commit() session.refresh(this_play) return this_play async def triples(session: Session, interaction: discord.Interaction, this_play: Play): """ Commits this play """ this_play.hit, this_play.triple, this_play.batter_final = 1, 1, 3 this_play = advance_runners(session, this_play, num_bases=3) session.add(this_play) session.commit() session.refresh(this_play) return this_play async def homeruns( session: Session, interaction: discord.Interaction, this_play: Play, homerun_type: Literal["ballpark", "no-doubt"], ): this_play.hit, this_play.homerun, this_play.batter_final, this_play.run = 1, 1, 4, 1 this_play.bphr = 1 if homerun_type == "ballpark" else 0 this_play = advance_runners(session, this_play, num_bases=4) this_play.rbi += 1 log_run_scored(session, this_play.batter, this_play) session.refresh(this_play) return this_play async def walks( session: Session, interaction: discord.Interaction, this_play: Play, walk_type: Literal["unintentional", "intentional"] = "unintentional", ): this_play.ab, this_play.bb, this_play.batter_final = 0, 1, 1 this_play.ibb = 1 if walk_type == "intentional" else 0 this_play = advance_runners(session, this_play, num_bases=1, only_forced=True) session.add(this_play) session.commit() session.refresh(this_play) return this_play async def strikeouts( session: Session, interaction: discord.Interaction, this_play: Play ): this_play.so, this_play.outs = 1, 1 this_play = advance_runners(session, this_play, num_bases=0) session.add(this_play) session.commit() session.refresh(this_play) return this_play async def popouts(session: Session, interaction: discord.Interaction, this_play: Play): this_play.outs = 1 this_play = advance_runners(session, this_play, num_bases=0) session.add(this_play) session.commit() session.refresh(this_play) return this_play async def hit_by_pitch( session: Session, interaction: discord.Interaction, this_play: Play ): this_play.ab, this_play.hbp = 0, 1 this_play.batter_final = 1 this_play = advance_runners(session, this_play, num_bases=1, only_forced=True) session.add(this_play) session.commit() session.refresh(this_play) return this_play async def bunts( session: Session, interaction: discord.Interaction, this_play: Play, bunt_type: Literal["sacrifice", "bad", "popout", "double-play", "defense"], ): this_play.ab = 1 if bunt_type != "sacrifice" else 0 this_play.sac = 1 if bunt_type == "sacrifice" else 0 this_play.outs = 1 if bunt_type == "sacrifice": this_play = advance_runners(session, this_play, num_bases=1) elif bunt_type == "popout": this_play = advance_runners(session, this_play, num_bases=0) elif bunt_type == "bad": this_play = advance_runners(session, this_play, num_bases=1) this_play.batter_final = 1 if this_play.on_third is not None: this_play.on_third_final = None elif this_play.on_second is not None: this_play.on_second_final = None elif this_play.on_first is not None: this_play.on_first_final = None elif bunt_type == "double-play": this_play = advance_runners(session, this_play, num_bases=0) this_play.outs = 2 if this_play.starting_outs < 2 else 1 if this_play.on_third is not None: this_play.on_third_final = None elif this_play.on_second is not None: this_play.on_second_final = None elif this_play.on_first is not None: this_play.on_first_final = None elif bunt_type == "defense": if this_play.on_third is not None: runner = this_play.on_third lead_base = 4 elif this_play.on_second is not None: runner = this_play.on_second lead_base = 3 elif this_play.on_first is not None: runner = this_play.on_first lead_base = 2 take_sure_out = await ask_confirm( interaction=interaction, question=f"Will you take the sure out at first or throw {TO_BASE[lead_base]} for **{runner.player.name}**?", custom_confirm_label="Out at first", custom_cancel_label=f"Throw {TO_BASE[lead_base]}", ) if take_sure_out: this_play.ab = 0 this_play.sac = 1 this_play = advance_runners(session, this_play, num_bases=1) else: view = ButtonOptions( responders=[interaction.user], timeout=30, labels=["Pitcher", "Catcher", "First Base", "Third Base", None], ) question = await interaction.channel.send( content="Which defender is fielding the bunt? This is determined by the first d6 in your AB roll.", view=view, ) await view.wait() if view.value: await question.delete() if view.value == "Pitcher": defender = this_play.pitcher elif view.value == "Catcher": defender = this_play.catcher elif view.value == "First Base": defender = get_one_lineup( session, this_play.game, this_play.batter.team, position="1B" ) elif view.value == "Third Base": defender = get_one_lineup( session, this_play.game, this_play.batter.team, position="3B" ) else: log_exception( NoPlayerResponseException, "I do not know which defender fielded that ball.", ) else: await question.edit( content="You keep thinking on it and try again.", view=None ) log_exception( NoPlayerResponseException, f"{interaction.user.name} did not know who was fielding the bunt.", ) def_pos = await get_position(session, defender.card, defender.position) lead_runner_out = await ask_confirm( interaction=interaction, question=f"{runner.player.name}'s safe range is **1->{runner.card.batterscouting.battingcard.running - 4 + def_pos.range}**. Is the runner out {AT_BASE[lead_base]}?", custom_confirm_label=f"Out {AT_BASE[lead_base]}", custom_cancel_label=f"Safe {AT_BASE[lead_base]}", ) if lead_runner_out: this_play = advance_runners(session, this_play, 1) this_play.batter_final = 1 if this_play.on_third is not None: this_play.on_third_final = None elif this_play.on_second is not None: this_play.on_second_final = None elif this_play.on_first is not None: this_play.on_first_final = None else: this_play.outs = 0 this_play.batter_final = 1 this_play = advance_runners(session, this_play, 1) else: log_exception(KeyError, f"Bunt type {bunt_type} is not yet implemented") session.add(this_play) session.commit() session.refresh(this_play) return this_play async def chaos( session: Session, interaction: discord.Interaction, this_play: Play, chaos_type: Literal["wild-pitch", "passed-ball", "balk", "pickoff"], ): """ Commits this_play """ this_play.pa, this_play.ab = 0, 0 if chaos_type == "wild-pitch": this_play = advance_runners(session, this_play, 1) this_play.rbi = 0 this_play.wild_pitch = 1 elif chaos_type == "passed-ball": this_play = advance_runners(session, this_play, 1) this_play.rbi = 0 this_play.passed_ball = 1 elif chaos_type == "balk": this_play = advance_runners(session, this_play, 1) this_play.rbi = 0 this_play.balk = 1 elif chaos_type == "pickoff": this_play = advance_runners(session, this_play, 0) this_play.pick_off = 1 this_play.outs = 1 if this_play.on_third: this_play.on_third_final = None elif this_play.on_second: this_play.on_second_final = None elif this_play.on_first: this_play.on_first_final = None session.add(this_play) session.commit() session.refresh(this_play) return this_play async def steals( session: Session, interaction: discord.Interaction, this_play: Play, steal_type: Literal["stolen-base", "caught-stealing", "steal-plus-overthrow"], to_base: Literal[2, 3, 4], ) -> Play: this_play = advance_runners(session, this_play, 0) this_play.pa = 0 if steal_type in ["stolen-base", "steal-plus-overthrow"]: this_play.sb = 1 this_play.error = 1 if steal_type == "steal-plus-overthrow" else 0 if to_base == 4 and this_play.on_third: this_play.runner = this_play.on_third this_play.on_third_final = 4 log_run_scored(session, this_play.on_third, this_play) if this_play.on_second: this_play.on_second_final = 3 if steal_type == "steal-plus-overthrow": this_play.on_second_final = 4 log_run_scored( session, this_play.on_second, this_play, is_earned=False ) if this_play.on_first: this_play.on_first_final = 2 if steal_type == "stolen-base" else 3 elif to_base == 3 and this_play.on_second: this_play.runner = this_play.on_second this_play.on_second_final = 3 if this_play.on_first: this_play.on_first_final = 2 if steal_type == "steal-plus-overthrow": this_play.on_second_final = 4 log_run_scored(session, this_play.on_second, this_play, is_earned=False) if this_play.on_first: this_play.on_first_final = 3 else: this_play.runner = this_play.on_first this_play.on_first_final = 2 if steal_type == "stolen-base" else 3 if steal_type == "steal-plus-overthrow" and this_play.on_third: this_play.on_third_final = 4 log_run_scored(session, this_play.on_third, this_play, is_earned=False) elif steal_type == "caught-stealing": this_play.outs = 1 if to_base == 4 and this_play.on_third: this_play.runner = this_play.on_third this_play.on_third_final = None if this_play.on_second: this_play.on_second_final = 3 if this_play.on_first: this_play.on_first_final = 2 elif to_base == 3 and this_play.on_second: this_play.runner = this_play.on_second this_play.on_second_final = None if this_play.on_first: this_play.on_first_final = 2 else: this_play.runner = this_play.on_first this_play.on_first_final = None session.add(this_play) session.commit() session.refresh(this_play) return this_play async def xchecks( session: Session, interaction: discord.Interaction, this_play: Play, position: str, debug: bool = False, ) -> Play: defense_team = this_play.pitcher.team this_defender = get_one_lineup( session, this_play.game, this_team=defense_team, position=position ) this_play.defender = this_defender this_play.check_pos = position def_alignment = this_play.managerai.defense_alignment(session, this_play.game) defender_is_in = def_alignment.defender_in(position) playing_in = False defender_embed = defense_team.embed defender_embed.title = f"{defense_team.sname} {position} Check" defender_embed.description = f"{this_defender.player.name}" defender_embed.set_image(url=this_defender.player.image) logger.info(f"defender_embed: {defender_embed}") # if not debug: # await interaction.edit_original_response(content=None, embeds=embeds) this_rating = await get_position(session, this_defender.card, position) logger.info(f"position rating: {this_rating}") if this_play.on_third is not None: if not this_play.ai_is_batting and defender_is_in: playing_in = True elif this_play.ai_is_batting: playing_in = await ask_confirm( interaction, question=f"Was {this_defender.card.player.name} playing in?", label_type="yes", ) if playing_in: this_rating.range = min(this_rating.range + 1, 5) this_roll = sa_fielding_roll(defense_team, this_play, position, this_rating) logger.info(f"this_roll: {this_roll}") if not debug: question = f"Looks like this is a **{this_roll.hit_result}**" if this_roll.is_chaos: question += " **rare play**" elif this_roll.error_result is not None: question += f" plus {this_roll.error_result}-base error" question += ". Is that correct?" await interaction.edit_original_response( content=None, embeds=[defender_embed, *this_roll.embeds] ) is_correct = await ask_confirm( interaction, question, label_type="yes", timeout=30, ) else: is_correct = True hit_result = this_roll.hit_result error_result = this_roll.error_result is_rare_play = this_roll.is_chaos logger.info( f"X-Check in Game #{this_play.game_id} at {this_play.check_pos} for {this_play.defender.card.player.name_with_desc} of the {this_play.pitcher.team.sname} / hit_result: {hit_result} / error_result: {error_result} / is_rare_play: {is_rare_play} / is_correct: {is_correct}" ) if not is_correct: logger.error(f"{interaction.user.name} says the result was wrong.") logger.info("Asking if there was a hit") allow_hit = await ask_confirm( interaction, f"Did **{this_defender.player.name}** allow a hit?", label_type="yes", ) if allow_hit: if position in ["1B", "2B", "3B", "SS", "P"]: hit_options = ["SI1", "SI2"] elif position in ["LF", "CF", "RF"]: hit_options = ["SI2", "DO2", "DO3", "TR"] else: hit_options = ["SI1", "SPD"] logger.info(f"Setting hit options to {hit_options}") else: if position in ["1B", "2B", "3B", "SS", "P"]: hit_options = ["G3#", "G3", "G2#", "G2", "G1"] elif position in ["LF", "CF", "RF"]: hit_options = ["F1", "F2", "F3"] else: hit_options = ["G3", "G2", "G1", "PO", "FO"] logger.info(f"Setting hit options to {hit_options}") new_hit_result = await ask_with_buttons( interaction, button_options=hit_options, question=f"Which result did **{this_defender.player.name}** allow?", ) if new_hit_result is None: logger.error("No hit result was returned") return logger.info(f"new hit result: {new_hit_result}") logger.info("Asking if there was an error") allow_error = await ask_confirm( interaction, f"Did **{this_defender.player.name}** commit an error?", label_type="yes", ) if allow_error: if position in ["1B", "2B", "3B", "SS", "P", "C"]: error_options = ["1 base", "2 bases"] else: error_options = ["1 base", "2 bases", "3 bases"] logger.info(f"Setting error options to {error_options}") new_error_result = await ask_with_buttons( interaction, button_options=error_options, question=f"How many bases was **{this_defender.player.name}**'s error?", ) if new_error_result is None: logger.error("No error result was returned") return else: if "1" in new_error_result: new_error_result = 1 elif "2" in new_error_result: new_error_result = 2 else: new_error_result = 3 else: new_error_result = None logger.info(f"new_error_result: {new_error_result}") logger.info("Setting hit and error results and continuing with processing.") hit_result = new_hit_result error_result = new_error_result logger.info( f'hit_result == "SPD" ({hit_result == "SPD"}) and not is_rare_play ({not is_rare_play})' ) if hit_result == "SPD" and not is_rare_play: logger.info("Non-rare play SPD check") runner_speed = this_play.batter.card.batterscouting.battingcard.running speed_embed = this_play.batter.team.embed speed_embed.title = "Catcher X-Check - Speed Check" speed_embed.description = f"{this_play.batter.player.name} Speed Check" speed_embed.add_field(name="Runner Speed", value=f"{runner_speed}") speed_embed.add_field(name="", value="", inline=False) speed_embed.add_field(name="Safe Range", value=f"1 - {runner_speed}") speed_embed.add_field(name="Out Range", value=f"{runner_speed + 1} - 20") this_roll = d_twenty_roll(this_play.batter.team, this_play.game) if this_roll.d_twenty <= runner_speed: result = "SAFE" else: result = "OUT" logger.info( f"SPD check roll: {this_roll.d_twenty} / runner_speed: {runner_speed} / result: {result}" ) await interaction.channel.send( content=None, embeds=[speed_embed, *this_roll.embeds] ) is_correct = await ask_confirm( interaction, f"Looks like **{this_play.batter.player.name}** is {result} at first! Is that correct?", label_type="yes", ) if is_correct: logger.info("Result is correct") if result == "OUT": hit_result = "G3" else: hit_result = "SI1" else: logger.info("Result is NOT correct") if result == "OUT": hit_result = "SI1" else: hit_result = "G3" logger.info(f"Final SPD check result: {hit_result}") if "#" in hit_result: logger.info("Checking if the # result becomes a hit") if this_play.ai_is_batting: if (position in ["1B", "3B", "P", "C"] and def_alignment.corners_in) or ( position in ["1B, 2B", "3B", "SS", "P", "C"] and def_alignment.infield_in ): hit_result = "SI2" elif this_play.on_base_code > 0: is_holding = False if not playing_in: if position == "1B" and this_play.on_first is not None: is_holding = await ask_confirm( interaction, question=f"Was {this_play.on_second.card.player.name} held at first base?", label_type="yes", ) # elif position == '3B' and this_play.on_second is not None: # is_holding = await ask_confirm( # interaction, # question=f'Was {this_play.on_second.card.player.name} held at second base?', # label_type='yes' # ) elif ( position == "2B" and ( this_play.on_first is not None or this_play.on_second is not None ) and ( this_play.batter.card.batterscouting.battingcard.hand == "R" or ( this_play.batter.card.batterscouting.battingcard.hand == "S" and this_play.pitcher.card.pitcherscouting.pitchingcard.hand == "L" ) ) ): if this_play.on_second is not None: is_holding = await ask_confirm( interaction, question=f"Was {this_play.on_second.card.player.name} held at second base?", label_type="yes", ) elif this_play.on_first is not None: is_holding = await ask_confirm( interaction, question=f"Was {this_play.on_first.card.player.name} held at first base?", label_type="yes", ) elif ( position == "SS" and ( this_play.on_first is not None or this_play.on_second is not None ) and ( this_play.batter.card.batterscouting.battingcard.hand == "L" or ( this_play.batter.card.batterscouting.battingcard.hand == "S" and this_play.pitcher.card.pitcherscouting.pitchingcard.hand == "R" ) ) ): if this_play.on_second is not None: is_holding = await ask_confirm( interaction, question=f"Was {this_play.on_second.card.player.name} held at second base?", label_type="yes", ) elif this_play.on_first is not None: is_holding = await ask_confirm( interaction, question=f"Was {this_play.on_first.card.player.name} held at first base?", label_type="yes", ) if is_holding or playing_in: hit_result = "SI2" if is_rare_play: logger.info("Is rare play") if hit_result == "SI1": this_play = await singles(session, interaction, this_play, "*") if this_play.on_first is None: this_play.error = 1 this_play.batter_final = 2 elif hit_result == "SI2": this_play = await singles(session, interaction, this_play, "**") this_play.batter_final = None this_play.outs = 1 elif "DO" in hit_result: this_play = await doubles(session, interaction, this_play, "***") this_play.batter_final = None this_play.outs = 1 elif hit_result == "TR": this_play = await triples(session, interaction, this_play) this_play.batter_final = 4 this_play.run = 1 this_play.error = 1 elif hit_result == "PO": this_play = advance_runners(session, this_play, 1, earned_bases=0) this_play.ab, this_play.error, this_play.batter_final = 1, 1, 1 elif hit_result == "FO": this_play = advance_runners( session, this_play, 1, is_error=True, only_forced=True ) this_play.ab, this_play.error, this_play.batter_final = 1, 1, 1 elif hit_result == "G1": if this_play.on_first is not None and this_play.starting_outs < 2: this_play = await gb_letter( session, interaction, this_play, "B", position=this_play.check_pos, defender_is_in=playing_in, ) else: this_play = await gb_letter( session, interaction, this_play, "A", position=this_play.check_pos, defender_is_in=playing_in, ) elif hit_result == "G2": if this_play.on_base_code > 0: this_play = await gb_letter( session, interaction, this_play, "C", position=this_play.check_pos, defender_is_in=playing_in, ) else: this_play = await gb_letter( session, interaction, this_play, "B", position=this_play.check_pos, defender_is_in=playing_in, ) elif hit_result == "G3": if this_play.on_base_code > 0: this_play = await singles(session, interaction, this_play, "*") else: this_play = await gb_letter( session, interaction, this_play, "C", position=this_play.check_pos, defender_is_in=playing_in, ) elif hit_result == "SPD": this_play = await singles(session, interaction, this_play, "*") elif hit_result == "F1": this_play.outs = 1 this_play.ab = 1 if this_play.on_third is None else 0 if this_play.on_base_code > 0 and this_play.starting_outs < 2: this_play = advance_runners(session, this_play, 1) if this_play.on_second is not None: this_play.on_second_final = 4 log_run_scored( session, this_play.on_second, this_play, is_earned=False ) elif this_play.on_first is not None: this_play.on_first_final = 3 elif hit_result == "F2": this_play.outs = 1 this_play.ab = 1 if this_play.on_third is None else 0 if this_play.on_base_code > 0 and this_play.starting_outs < 2: this_play.on_third_final = None this_play.outs = 2 else: this_play.outs = 1 this_play.ab = 1 if this_play.on_third: this_play.outs = 2 this_play.on_third_final = None elif this_play.on_second: this_play.outs = 2 this_play.on_second_final = None elif this_play.on_first: this_play.outs = 2 this_play.on_first_final = None elif hit_result not in ["SI1", "SI2", "DO2", "DO3", "TR"] and error_result is None: logger.info("Not a hit, not an error") if this_play.on_base_code == 0: this_play = await gb_result(session, interaction, this_play, 1) else: to_mif = position in ["2B", "SS"] to_right_side = position in ["1B", "2B"] if "G3" in hit_result: if this_play.on_base_code == 2 and not playing_in: this_play = await gb_result(session, interaction, this_play, 12) elif playing_in and this_play.on_base_code == 5: this_play = await gb_result( session, interaction, this_play, 7, to_mif, to_right_side ) elif playing_in and this_play.on_base_code in [3, 6]: this_play = await gb_decide( session, interaction=interaction, this_play=this_play ) elif playing_in and this_play.on_base_code == 7: this_play = await gb_result(session, interaction, this_play, 11) else: this_play = await gb_result(session, interaction, this_play, 3) elif "G2" in hit_result: if this_play.on_base_code == 7 and playing_in: this_play = await gb_result(session, interaction, this_play, 11) elif not playing_in and this_play.on_base_code in [3, 6]: this_play = await gb_result( session, interaction, this_play, 5, to_mif=to_mif ) elif playing_in and this_play.on_base_code in [3, 5, 6]: this_play = await gb_result(session, interaction, this_play, 1) elif this_play.on_base_code == 2: this_play = await gb_result(session, interaction, this_play, 12) else: this_play = await gb_result(session, interaction, this_play, 4) elif "G1" in hit_result: if this_play.on_base_code == 7 and playing_in: this_play = await gb_result(session, interaction, this_play, 10) elif not playing_in and this_play.on_base_code == 4: this_play = await gb_result(session, interaction, this_play, 13) elif not playing_in and this_play.on_base_code in [3, 6]: this_play = await gb_result(session, interaction, this_play, 3) elif playing_in and this_play.on_base_code in [3, 5, 6]: this_play = await gb_result(session, interaction, this_play, 1) elif this_play.on_base_code == 2: this_play = await gb_result(session, interaction, this_play, 12) else: this_play = await gb_result(session, interaction, this_play, 2) elif "F1" in hit_result: this_play = await flyballs(session, interaction, this_play, "a") elif "F2" in hit_result: this_play = await flyballs(session, interaction, this_play, "b") elif "F3" in hit_result: this_play = await flyballs(session, interaction, this_play, "c") # FO and PO else: this_play.ab, this_play.outs = 1, 1 this_play = advance_runners(session, this_play, 0) elif ( hit_result not in ["SI1", "SI2", "DO2", "DO3", "TR"] and error_result is not None ): logger.info(f"Not a hit, {error_result}-base error") this_play = advance_runners(session, this_play, error_result, earned_bases=0) this_play.ab, this_play.error, this_play.batter_final = 1, 1, error_result else: logger.info(f"Hit result: {hit_result}, Error: {error_result}") if hit_result == "SI1" and error_result is None: this_play = await singles(session, interaction, this_play, "*") elif hit_result == "SI1": this_play.ab, this_play.hit, this_play.error, this_play.batter_final = ( 1, 1, 1, 2, ) this_play = advance_runners( session, this_play, num_bases=error_result + 1, earned_bases=1 ) elif hit_result == "SI2" and error_result is None: this_play = await singles(session, interaction, this_play, "**") elif hit_result == "SI2": this_play.ab, this_play.hit, this_play.error = 1, 1, 1 if error_result > 1: num_bases = 3 this_play.batter_final = 3 else: num_bases = 2 this_play.batter_final = 2 this_play = advance_runners( session, this_play, num_bases=num_bases, earned_bases=2 ) elif hit_result == "DO2" and error_result is None: this_play = await doubles(session, interaction, this_play, "**") elif hit_result == "DO2": this_play.ab, this_play.hit, this_play.error, this_play.double = 1, 1, 1, 1 num_bases = 3 if error_result == 3: this_play.batter_final = 4 else: this_play.batter_final = 3 this_play = advance_runners( session, this_play, num_bases=num_bases, earned_bases=2 ) elif hit_result == "DO3" and error_result is None: this_play = await doubles(session, interaction, this_play, "***") elif hit_result == "DO3": this_play.ab, this_play.hit, this_play.error, this_play.double = 1, 1, 1, 1 if error_result == 1: this_play.batter_final = 3 else: this_play.batter_final = 4 this_play = advance_runners(session, this_play, num_bases=4, earned_bases=2) elif hit_result == "TR" and error_result is None: this_play = await triples(session, interaction, this_play) else: ( this_play.ab, this_play.hit, this_play.error, this_play.run, this_play.triple, this_play.batter_final, ) = ( 1, 1, 1, 1, 1, 4, ) this_play = advance_runners(session, this_play, num_bases=4, earned_bases=3) session.add(this_play) session.commit() session.refresh(this_play) return this_play def activate_last_play(session: Session, this_game: Game) -> Play: logger.info("Pulling last play to complete and advance") p_query = session.exec( select(Play) .where(Play.game == this_game) .order_by(Play.play_num.desc()) .limit(1) ).all() logger.info(f"last play: {p_query[0].id}") this_play = complete_play(session, p_query[0]) return this_play def undo_play(session: Session, this_play: Play): this_game = this_play.game after_play_min = max(1, this_play.play_num - 2) last_two_plays = session.exec( select(Play) .where(Play.game == this_game) .order_by(Play.play_num.desc()) .limit(2) ).all() for play in last_two_plays: for runner, to_base in [ (play.on_first, play.on_first_final), (play.on_second, play.on_second_final), (play.on_third, play.on_third_final), ]: if to_base == 4: last_pa = get_players_last_pa(session, runner) last_pa.run, last_pa.e_run = 0, 0 session.add(last_pa) last_two_ids = [last_two_plays[0].id, last_two_plays[1].id] logger.warning(f"Deleting plays: {last_two_ids}") session.exec(delete(Play).where(Play.id.in_(last_two_ids))) new_player_ids = [] new_players = session.exec( select(Lineup).where( Lineup.game == this_game, Lineup.after_play >= after_play_min ) ).all() logger.info(f"Subs to roll back: {new_players}") for x in new_players: logger.info(f"Marking {x} for deletion") new_player_ids.append(x.id) old_player = session.get(Lineup, x.replacing_id) old_player.active = True session.add(old_player) logger.warning(f"Deleting lineup IDs: {new_player_ids}") session.exec(delete(Lineup).where(Lineup.id.in_(new_player_ids))) session.commit() try: logger.info(f"Attempting to initialize play for Game {this_game.id}...") this_play = this_game.initialize_play(session) logger.info(f"Initialized play: {this_play.id}") except PlayInitException: logger.info("Plays found, attempting to active the last play") this_play = activate_last_play(session, this_game) logger.info(f"Re-activated play: {this_play.id}") return this_play async def show_defense_cards( session: Session, interaction: discord.Interaction, this_play: Play, first_position: DEFENSE_LITERAL, ): position_map = { "Pitcher": "P", "Catcher": "C", "First Base": "1B", "Second Base": "2B", "Third Base": "3B", "Shortstop": "SS", "Left Field": "LF", "Center Field": "CF", "Right Field": "RF", } this_position = position_map[first_position] sorted_lineups = get_sorted_lineups(session, this_play.game, this_play.pitcher.team) select_player_options = [ discord.SelectOption( label=f"{x.position} - {x.player.name}", value=f"{x.id}", default=this_position == x.position, ) for x in sorted_lineups ] this_lineup = get_one_lineup( session, this_play.game, this_play.pitcher.team, position=this_position ) player_embed = image_embed( image_url=this_lineup.player.image, color=this_play.pitcher.team.color, author_name=this_play.pitcher.team.lname, author_icon=this_play.pitcher.team.logo, ) player_dropdown = SelectViewDefense( options=select_player_options, this_play=this_play, base_embed=player_embed, session=session, sorted_lineups=sorted_lineups, responders=[interaction.user], ) dropdown_view = DropdownView(dropdown_objects=[player_dropdown], timeout=60) await interaction.edit_original_response( content=None, embed=player_embed, view=dropdown_view ) def is_game_over(this_play: Play) -> bool: if this_play.inning_num < 9 and ( abs(this_play.away_score - this_play.home_score) < 10 ): return False if abs(this_play.away_score - this_play.home_score) >= 10: if ( (this_play.home_score - this_play.away_score) >= 10 ) and this_play.inning_half == "bot": return True elif ( ((this_play.away_score - this_play.home_score) >= 10) and this_play.is_new_inning and this_play.inning_half == "top" ): return True if ( this_play.inning_num > 9 and this_play.inning_half == "top" and this_play.is_new_inning and this_play.home_score != this_play.away_score ): return True if ( this_play.inning_num >= 9 and this_play.inning_half == "bot" and this_play.home_score > this_play.away_score ): return True return False async def get_game_summary_embed( session: Session, interaction: discord.Interaction, this_play: Play, db_game_id: int, winning_team: Team, losing_team: Team, num_potg: int = 1, num_poop: int = 0, ): game_summary = await db_get( f"plays/game-summary/{db_game_id}", params=[("tp_max", num_potg)] ) this_game = this_play.game game_embed = winning_team.embed game_embed.title = f"{this_game.away_team.lname} {this_play.away_score} @ {this_play.home_score} {this_game.home_team.lname} - F/{this_play.inning_num}" game_embed.add_field( name="Location", value=f"{interaction.guild.get_channel(this_game.channel_id).mention}", ) game_embed.add_field(name="Game ID", value=f"{db_game_id}") if this_game.game_type == "major-league": game_des = "Major League" elif this_game.game_type == "minor-league": game_des = "Minor League" elif this_game.game_type == "hall-of-fame": game_des = "Hall of Fame" elif this_game.game_type == "flashback": game_des = "Flashback" elif this_game.ranked: game_des = "Ranked" elif "gauntlet" in this_game.game_type: game_des = "Gauntlet" else: game_des = "Unlimited" game_embed.description = f"Score Report - {game_des}" game_embed.add_field( name="Box Score", value=f"```\n" f"Team | R | H | E |\n" f"{this_game.away_team.abbrev.replace('Gauntlet-', ''): <4} | {game_summary['runs']['away']: >2} | " f"{game_summary['hits']['away']: >2} | {game_summary['errors']['away']: >2} |\n" f"{this_game.home_team.abbrev.replace('Gauntlet-', ''): <4} | {game_summary['runs']['home']: >2} | " f"{game_summary['hits']['home']: >2} | {game_summary['errors']['home']: >2} |\n" f"\n```", inline=False, ) logger.info("getting top players string") potg_string = "" for tp in game_summary["top-players"]: player_name = f"{get_player_name_from_dict(tp['player'])}" potg_line = f"{player_name} - " if "hr" in tp: potg_line += f"{tp['hit']}-{tp['ab']}" if tp["hr"] > 0: num = f"{tp['hr']} " if tp["hr"] > 1 else "" potg_line += f", {num}HR" if tp["triple"] > 0: num = f"{tp['triple']} " if tp["triple"] > 1 else "" potg_line += f", {num}3B" if tp["double"] > 0: num = f"{tp['double']} " if tp["double"] > 1 else "" potg_line += f", {num}2B" if tp["run"] > 0: potg_line += f", {tp['run']} R" if tp["rbi"] > 0: potg_line += f", {tp['rbi']} RBI" else: potg_line = f"{player_name} - {tp['ip']} IP, {tp['run']} R" if tp["run"] != tp["e_run"]: potg_line += f" ({tp['e_run']} ER)" potg_line += f", {tp['hit']} H, {tp['so']} K" potg_line += f", {tp['re24']:.2f} re24\n" potg_string += potg_line game_embed.add_field(name="Players of the Game", value=potg_string, inline=False) logger.info("getting pooper string") poop_string = "" if "pooper" in game_summary and game_summary["pooper"] is not None: if isinstance(game_summary["pooper"], dict): all_poop = [game_summary["pooper"]] elif isinstance(game_summary["pooper"], list): all_poop = game_summary["pooper"] for line in all_poop: player_name = f"{get_player_name_from_dict(line['player'])}" poop_line = f"{player_name} - " if "hr" in line: poop_line += f"{line['hit']}-{line['ab']}" else: poop_line += f"{line['ip']} IP, {line['run']} R" if tp["run"] != line["e_run"]: poop_line += f" ({line['e_run']} ER)" poop_line += f", {line['hit']} H, {line['so']} K" poop_line += f", {line['re24']:.2f} re24\n" poop_string += poop_line if len(poop_string) > 0: game_embed.add_field(name="Pooper of the Game", value=poop_string, inline=False) pit_string = f"Win: {game_summary['pitchers']['win']['p_name']}\nLoss: {game_summary['pitchers']['loss']['p_name']}\n" hold_string = None for player in game_summary["pitchers"]["holds"]: player_name = f"{get_player_name_from_dict(player)}" if hold_string is None: hold_string = f"Holds: {player_name}" else: hold_string += f", {player_name}" if hold_string is not None: pit_string += f"{hold_string}\n" if game_summary["pitchers"]["save"] is not None: player_name = f"{get_player_name_from_dict(game_summary['pitchers']['save'])}" pit_string += f"Save: {player_name}" game_embed.add_field( name="Pitching", value=pit_string, ) def name_list(raw_list: list) -> str: logger.info(f"raw_list: {raw_list}") player_dict = {} for x in raw_list: if x["player_id"] not in player_dict: player_dict[x["player_id"]] = x data_dict = {} for x in raw_list: if x["player_id"] not in data_dict: data_dict[x["player_id"]] = 1 else: data_dict[x["player_id"]] += 1 r_string = "" logger.info(f"players: {player_dict} / data: {data_dict}") first = True for p_id in data_dict: r_string += f"{', ' if not first else ''}{player_dict[p_id]['p_name']}" if data_dict[p_id] > 1: r_string += f" {data_dict[p_id]}" first = False return r_string logger.info("getting running string") if len(game_summary["running"]["sb"]) + len(game_summary["running"]["csc"]) > 0: run_string = "" if len(game_summary["running"]["sb"]) > 0: run_string += f"SB: {name_list(game_summary['running']['sb'])}\n" if len(game_summary["running"]["csc"]) > 0: run_string += f"CSc: {name_list(game_summary['running']['csc'])}" game_embed.add_field(name="Baserunning", value=run_string) logger.info("getting xbh string") if ( len(game_summary["xbh"]["2b"]) + len(game_summary["xbh"]["3b"]) + len(game_summary["xbh"]["hr"]) > 0 ): bat_string = "" if len(game_summary["xbh"]["2b"]) > 0: bat_string += f"2B: {name_list(game_summary['xbh']['2b'])}\n" if len(game_summary["xbh"]["3b"]) > 0: bat_string += f"3B: {name_list(game_summary['xbh']['3b'])}\n" if len(game_summary["xbh"]["hr"]) > 0: bat_string += f"HR: {name_list(game_summary['xbh']['hr'])}\n" else: bat_string = "Oops! All bitches! No XBH from either team." game_embed.add_field(name="Batting", value=bat_string, inline=False) return game_embed async def complete_game( session: Session, interaction: discord.Interaction, this_play: Play, bot: discord.Client = None, ): # if interaction is not None: # salutation = await interaction.channel.send('GGs, I\'ll tally this game up...') # Add button with {winning_team} wins! and another with "Roll Back" this_game = this_play.game async def roll_back( db_game_id: int, game: bool = True, plays: bool = False, decisions: bool = False ): if decisions: try: await db_delete("decisions/game", object_id=db_game_id) except DatabaseError as e: logger.warning(f"Could not delete decisions for game {db_game_id}: {e}") if plays: try: await db_delete("plays/game", object_id=db_game_id) except DatabaseError as e: logger.warning(f"Could not delete plays for game {db_game_id}: {e}") if game: try: await db_delete("games", object_id=db_game_id) except DatabaseError as e: logger.warning(f"Could not delete game {db_game_id}: {e}") # Post completed game to API game_data = this_game.model_dump() game_data["home_team_ranking"] = this_game.home_team.ranking game_data["away_team_ranking"] = this_game.away_team.ranking game_data["home_team_value"] = this_game.home_team.team_value game_data["away_team_value"] = this_game.away_team.team_value game_data["away_score"] = this_play.away_score game_data["home_score"] = this_play.home_score winning_team = ( this_game.home_team if this_play.home_score > this_play.away_score else this_game.away_team ) losing_team = ( this_game.home_team if this_play.away_score > this_play.home_score else this_game.away_team ) try: db_game = await db_post("games", payload=game_data) db_ready_plays = get_db_ready_plays(session, this_game, db_game["id"]) db_ready_decisions = get_db_ready_decisions(session, this_game, db_game["id"]) except Exception as e: await roll_back(db_game["id"]) log_exception(e, msg="Unable to post game to API, rolling back") # Post game stats to API try: resp = await db_post("plays", payload=db_ready_plays) except Exception as e: await roll_back(db_game["id"], plays=True) log_exception(e, msg="Unable to post plays to API, rolling back") if len(resp) > 0: pass try: resp = await db_post("decisions", payload={"decisions": db_ready_decisions}) except Exception as e: await roll_back(db_game["id"], plays=True, decisions=True) log_exception(e, msg="Unable to post decisions to API, rolling back") if len(resp) > 0: pass # Post game rewards (gauntlet and main team) try: win_reward, loss_reward = await post_game_rewards( session, winning_team=winning_team, losing_team=losing_team, this_game=this_game, ) if "gauntlet" in this_game.game_type: logger.info("Posting gauntlet results") await post_result( run_id=int(this_game.game_type.split("-")[3]), is_win=winning_team.gmid == interaction.user.id, this_team=this_game.human_team, bot=bot, channel=interaction.channel, responders=[interaction.user], ) except Exception as e: await roll_back(db_game["id"], plays=True, decisions=True) log_exception(e, msg="Error while posting game rewards") session.delete(this_play) session.commit() # Pull game summary for embed summary_embed = await get_game_summary_embed( session, interaction, this_play, db_game["id"], winning_team=winning_team, losing_team=losing_team, num_potg=3, num_poop=1, ) summary_embed.add_field(name=f"{winning_team.abbrev} Rewards", value=win_reward) summary_embed.add_field(name=f"{losing_team.abbrev} Rewards", value=loss_reward) summary_embed.add_field( name="Highlights", value=f"Please share the highlights in {get_channel(interaction, 'pd-news-ticker').mention}!", inline=False, ) # Create and post game summary to game channel and pd-network-news news_ticker = get_channel(interaction, "pd-network-news") if news_ticker is not None: await news_ticker.send(content=None, embed=summary_embed) # await interaction.channel.send(content=None, embed=summary_embed) await interaction.edit_original_response(content=None, embed=summary_embed) game_id = this_game.id this_game.active = False session.add(this_game) session.commit() logger.info(f"Just ended game {game_id}") async def update_game_settings( session: Session, interaction: discord.Interaction, this_game: Game, roll_buttons: bool = None, auto_roll: bool = None, ) -> discord.Embed: if roll_buttons is not None: this_game.roll_buttons = roll_buttons if auto_roll is not None: this_game.auto_roll = auto_roll session.add(this_game) session.commit() session.refresh(this_game) this_team = ( this_game.away_team if this_game.away_team.gmid == interaction.user.id else this_game.home_team ) embed = this_team.embed embed.title = f"Game Settings - {this_team.lname}" embed.add_field( name="Roll Buttons", value=f"{'ON' if this_game.roll_buttons else 'OFF'}" ) embed.add_field(name="Auto Roll", value=f"{'ON' if this_game.auto_roll else 'OFF'}") return embed async def manual_end_game( session: Session, interaction: discord.Interaction, this_game: Game, current_play: Play, ): logger.info(f"manual_end_game - Game {this_game.id}") GAME_DONE_STRING = "Okay, it's gone. You're free to start another one!" GAME_STAYS_STRING = "No problem, this game will continue!" if not is_game_over(current_play): logger.info("manual_end_game - game is not over") if ( current_play.inning_num == 1 and current_play.play_num < 3 and "gauntlet" not in this_game.game_type.lower() ): logger.info( f"manual_end_game - {this_game.game_type} game just started, asking for confirmation" ) await interaction.edit_original_response( content="Looks like this game just started." ) cancel_early = await ask_confirm( interaction, "Are you sure you want to cancel it?", label_type="yes", timeout=30, delete_question=False, ) if cancel_early: logger.info(f"{interaction.user.name} is cancelling the game") await interaction.channel.send(content=GAME_DONE_STRING) news_ticker = get_channel(interaction, "pd-network-news") if news_ticker is not None: await news_ticker.send( content=f"{interaction.user.display_name} had dinner plans so had to end their game down in {interaction.channel.mention} early." ) this_game.active = False session.add(this_game) session.commit() else: logger.info(f"{interaction.user.name} is not cancelling the game") await interaction.channel.send_message(content=GAME_STAYS_STRING) return else: logger.info( f"manual_end_game - {this_game.game_type} game currently in inning #{current_play.inning_num}, asking for confirmation" ) await interaction.edit_original_response( content="It doesn't look like this game isn't over, yet. I can end it, but no rewards will be paid out and you will take the L." ) forfeit_game = await ask_confirm( interaction, "Should I end this game?", label_type="yes", timeout=30, delete_question=False, ) if forfeit_game: logger.info(f"{interaction.user.name} is forfeiting the game") game_data = this_game.model_dump() game_data["home_team_ranking"] = this_game.home_team.ranking game_data["away_team_ranking"] = this_game.away_team.ranking game_data["home_team_value"] = this_game.home_team.team_value game_data["away_team_value"] = this_game.away_team.team_value game_data["away_score"] = current_play.away_score game_data["home_score"] = current_play.home_score game_data["forfeit"] = True try: await db_post("games", payload=game_data) except Exception: logger.error("Unable to post forfeited game") await interaction.channel.send(content=GAME_DONE_STRING) news_ticker = get_channel(interaction, "pd-network-news") if news_ticker is not None: await news_ticker.send( content=f"{interaction.user.display_name} escorts the {this_game.human_team.sname} out of {interaction.channel.mention} in protest." ) this_game.active = False session.add(this_game) session.commit() else: logger.info(f"{interaction.user.name} is not forfeiting the game") await interaction.channel.send(content=GAME_STAYS_STRING) return # else: # logger.info(f'manual_end_game - gauntlet game currently in inning #{current_play.inning_num}, asking for confirmation') # await interaction.edit_original_response(content='It doesn\'t look like this game is over, yet. I can end it, but no rewards will be paid out and you will take the L.') else: logger.info("manual_end_game - game is over") await complete_game(session, interaction, current_play) async def groundballs( session: Session, interaction: discord.Interaction, this_play: Play, groundball_letter: Literal["a", "b", "c"], ): if this_play.starting_outs == 2: return await gb_result(session, interaction, this_play, 1) if this_play.on_base_code == 2 and groundball_letter in ["a", "b"]: logger.info(f"Groundball {groundball_letter} with runner on second") to_right_side = await ask_confirm( interaction, question="Was that ball hit to either 1B or 2B?", label_type="yes", ) this_play = gb_result_6(session, this_play, to_right_side) elif this_play.on_base_code in [3, 6] and groundball_letter in ["a", "b"]: logger.info(f"Groundball {groundball_letter} with runner on third") def_alignment = this_play.managerai.defense_alignment(session, this_play.game) if this_play.game.ai_team is not None and this_play.pitcher.team.is_ai: if def_alignment.infield_in: logger.info("AI on defense, playing in") if groundball_letter == "a": this_play = gb_result_7(session, this_play) else: this_play = gb_result_1(session, this_play) else: logger.info("AI on defense, playing back") to_mif = await ask_confirm( interaction, question="Was that ball hit to either 2B or SS?", label_type="yes", ) if to_mif or not def_alignment.corners_in: logger.info("playing back, gb 5") this_play = gb_result_5(session, this_play, to_mif) else: logger.info("corners in, gb 7") this_play = gb_result_7(session, this_play) else: logger.info("Checking if hit to MIF") to_mif = await ask_confirm( interaction, question="Was that ball hit to either 2B or SS?", label_type="yes", ) if to_mif: playing_in = await ask_confirm( interaction, question="Were they playing in?", label_type="yes", ) if playing_in: logger.info("To MIF, batter out, runners hold") this_play = gb_result_1(session, this_play) else: logger.info("To MIF, playing back, gb 3") this_play = gb_result_3(session, this_play) else: logger.info("Batter out, runners hold") this_play = gb_result_1(session, this_play) elif this_play.on_base_code in [5, 7] and groundball_letter == "a": logger.info(f"Groundball {groundball_letter} with runners on including third") if this_play.game.ai_team is not None and this_play.pitcher.team.is_ai: def_alignment = this_play.managerai.defense_alignment( session, this_play.game ) logger.info(f"def_alignment: {def_alignment}") if def_alignment.infield_in: if this_play.on_base_code == 5: logger.info("playing in, gb 7") this_play = gb_result_7(session, this_play) else: logger.info("playing in, gb 10") this_play = gb_result_10(session, this_play) elif def_alignment.corners_in: logger.info("Checking if ball was hit to 1B/3B") to_cif = await ask_confirm( interaction, "Was that ball hit to 1B/3B?", label_type="yes" ) if to_cif: if this_play.on_base_code == 5: logger.info("Corners in, gb 7") this_play = gb_result_7(session, this_play) else: logger.info("Corners in, gb 10") this_play = gb_result_10(session, this_play) else: logger.info("Corners back, gb 2") this_play = gb_result_2(session, this_play) else: logger.info("playing back, gb 2") this_play = gb_result_2(session, this_play) else: playing_in = await ask_confirm( interaction, question="Was the defender playing in?", label_type="yes" ) if playing_in and this_play.on_base_code == 5: logger.info("playing in, gb 7") this_play = gb_result_7(session, this_play) elif playing_in: logger.info("playing in, gb 10") this_play = gb_result_10(session, this_play) else: logger.info("playing back, gb 2") this_play = gb_result_2(session, this_play) elif this_play.on_base_code in [5, 7] and groundball_letter == "b": logger.info(f"Groundball {groundball_letter} with runners on including third") playing_in = False if this_play.game.ai_team is not None and this_play.pitcher.team.is_ai: def_alignment = this_play.managerai.defense_alignment( session, this_play.game ) logger.info(f"def_alignment: {def_alignment}") to_mif = await ask_confirm( interaction, question="Was that hit to 2B/SS?", label_type="yes" ) if def_alignment.infield_in or not to_mif and def_alignment.corners_in: playing_in = True else: playing_in = await ask_confirm( interaction, question="Was the defender playing in?", label_type="yes" ) if playing_in and this_play.on_base_code == 7: logger.info("playing in, gb 11") this_play = gb_result_11(session, this_play) elif playing_in: logger.info("playing in, gb 9") this_play = gb_result_9(session, this_play) else: logger.info("playing back, gb 4") this_play = gb_result_4(session, this_play) else: if this_play.on_base_code in [3, 5, 6, 7]: def_align = this_play.managerai.defense_alignment(session, this_play.game) if def_align.infield_in: playing_in = True else: to_mif = await ask_confirm( interaction, question="Was that ball hit to 2B/SS?", label_type="yes", ) if not to_mif and def_align.corners_in: playing_in = True else: playing_in = False else: playing_in = False this_play = await gb_letter( session, interaction, this_play, groundball_letter.upper(), "None", playing_in, ) session.add(this_play) session.commit() session.refresh(this_play) return this_play async def gb_letter( session: Session, interaction: discord.Interaction, this_play: Play, groundball_letter: Literal["A", "B", "C"], position: str, defender_is_in: bool, ): """ Commits this_play """ if not defender_is_in: if this_play.on_base_code == 0: return await gb_result(session, interaction, this_play, 1) elif groundball_letter == "C": return await gb_result(session, interaction, this_play, 3) elif groundball_letter == "A" and this_play.on_base_code in [1, 4, 5, 7]: return await gb_result(session, interaction, this_play, 2) elif groundball_letter == "B" and this_play.on_base_code in [1, 4, 5, 7]: return await gb_result(session, interaction, this_play, 4) elif this_play.on_base_code in [3, 6]: return await gb_result( session, interaction, this_play, 5, to_mif=position in ["2B", "SS"] ) else: return await gb_result( session, interaction, this_play, 6, to_right_side=position in ["1B", "2B"], ) else: if groundball_letter == "A" and this_play.on_base_code == 7: return await gb_result(session, interaction, this_play, 10) elif groundball_letter == "B" and this_play.on_base_code == 5: return await gb_result(session, interaction, this_play, 9) elif this_play.on_base_code == 7: return await gb_result(session, interaction, this_play, 11) elif groundball_letter == "A": return await gb_result(session, interaction, this_play, 7) elif groundball_letter == "B": return await gb_result(session, interaction, this_play, 1) else: return await gb_result(session, interaction, this_play, 8) async def gb_result( session: Session, interaction: discord.Interaction, this_play: Play, groundball_result: int, to_mif: bool = None, to_right_side: bool = None, ): """ Commits this_play Result 5 requires to_mif Result 6 requires to_right_side """ logger.info( f"Starting a groundball result: GB #{groundball_result}, to_mif: {to_mif}, to_right_side: {to_right_side}" ) if groundball_result == 1: this_play = gb_result_1(session, this_play) elif groundball_result == 2: this_play = gb_result_2(session, this_play) elif groundball_result == 3: this_play = gb_result_3(session, this_play) elif groundball_result == 4: this_play = gb_result_4(session, this_play) elif groundball_result == 5: this_play = gb_result_5(session, this_play, to_mif) elif groundball_result == 6: this_play = gb_result_6(session, this_play, to_right_side) elif groundball_result == 7: this_play = gb_result_7(session, this_play) elif groundball_result == 8: this_play = gb_result_8(session, this_play) elif groundball_result == 9: this_play = gb_result_9(session, this_play) elif groundball_result == 10: this_play = gb_result_10(session, this_play) elif groundball_result == 11: this_play = gb_result_11(session, this_play) elif groundball_result == 12: this_play = await gb_result_12(session, this_play, interaction) elif groundball_result == 13: this_play = gb_result_13(session, this_play) session.add(this_play) session.commit() session.refresh(this_play) return this_play def gb_result_1(session: Session, this_play: Play): logger.info("GB 1") this_play = advance_runners(session, this_play, 0) this_play.ab, this_play.outs = 1, 1 return this_play def gb_result_2(session: Session, this_play: Play): logger.info("GB 2") num_outs = 2 if this_play.starting_outs <= 1 else 1 this_play.ab, this_play.outs = 1, num_outs this_play.on_first_final = None if num_outs + this_play.starting_outs < 3: if this_play.on_second: this_play.on_second_final = 3 if this_play.on_third: this_play.on_third_final = 4 log_run_scored(session, runner=this_play.on_third, this_play=this_play) return this_play def gb_result_3(session: Session, this_play: Play): logger.info("GB 3") if this_play.starting_outs < 2: this_play = advance_runners(session, this_play, 1) this_play.ab, this_play.outs = 1, 1 return this_play def gb_result_4(session: Session, this_play: Play): logger.info("GB 4") if this_play.starting_outs < 2: this_play = advance_runners(session, this_play, 1) this_play.ab, this_play.outs = 1, 1 this_play.on_first_final = None this_play.batter_final = 1 return this_play def gb_result_5(session: Session, this_play: Play, to_mif: bool): logger.info("GB 5") this_play.ab, this_play.outs = 1, 1 if to_mif: this_play = gb_result_3(session, this_play) else: this_play = gb_result_1(session, this_play) return this_play def gb_result_6(session: Session, this_play: Play, to_right_side: bool): logger.info("GB 6") this_play.ab, this_play.outs = 1, 1 if to_right_side: this_play = gb_result_3(session, this_play) else: this_play = gb_result_1(session, this_play) return this_play def gb_result_7(session: Session, this_play: Play): logger.info("GB 7") this_play.ab, this_play.outs = 1, 1 this_play = advance_runners(session, this_play, num_bases=1, only_forced=True) return this_play def gb_result_8(session: Session, this_play: Play): logger.info("GB 8") return gb_result_7(session, this_play) def gb_result_9(session: Session, this_play: Play): logger.info("GB 9") this_play.ab, this_play.outs = 1, 1 this_play.on_third_final = 3 this_play.on_first_final = 2 return this_play def gb_result_10(session: Session, this_play: Play): logger.info("GB 10") num_outs = 2 if this_play.starting_outs <= 1 else 1 this_play.ab, this_play.outs = 1, num_outs this_play.on_second_final = 3 this_play.on_first_final = 2 return this_play def gb_result_11(session: Session, this_play: Play): logger.info("GB 11") this_play.ab, this_play.outs = 1, 1 this_play.on_first_final = 2 this_play.on_second_final = 3 this_play.batter_final = 1 return this_play async def gb_decide( session: Session, this_play: Play, interaction: discord.Interaction ): logger.info("GB Decide") runner = ( this_play.on_third if this_play.on_third is not None else this_play.on_second ) logger.info(f"runner: {runner}") pos_rating = await get_position( session, this_play.defender.card, this_play.check_pos ) safe_range = runner.card.batterscouting.battingcard.running - 4 + pos_rating.range advance_base = 4 if this_play.on_third is not None else 3 logger.info( f"pos_rating: {pos_rating}\nsafe_range: {safe_range}\nadvance_base: {advance_base}" ) if this_play.game.ai_team is not None and this_play.ai_is_batting: run_resp = this_play.managerai.gb_decide_run(session, this_play.game) if this_play.on_second is None and this_play.on_third is None: log_exception( InvalidResultException, "Cannot run GB Decide without a runner on base." ) if safe_range >= run_resp.min_safe: is_lead_running = True else: is_lead_running = False else: is_lead_running = await ask_confirm( interaction, f"Is **{runner.card.player.name}** attempting to advance {TO_BASE[advance_base]} with a **1-{safe_range}** safe range?", label_type="yes", delete_question=False, ) if not is_lead_running: this_play = advance_runners(session, this_play, 0) this_play.outs = 1 else: if this_play.game.ai_team is not None and not this_play.ai_is_batting: throw_resp = this_play.managerai.gb_decide_throw( session, this_play.game, runner_speed=runner.card.batterscouting.battingcard.running, defender_range=pos_rating.range, ) throw_for_lead = throw_resp.at_lead_runner await interaction.channel.send( content=f"**{this_play.defender.player.name}** is {'not ' if not throw_for_lead else ''}throwing for {runner.player.name}{'!' if throw_for_lead else '.'}" ) else: throw_for_lead = await ask_confirm( interaction, f"Is {this_play.defender.player.name} throwing for {runner.player.name}?", label_type="yes", ) if not throw_for_lead: if this_play.starting_outs < 2: this_play = advance_runners(session, this_play, 1) this_play.outs = 1 else: is_lead_out = await ask_confirm( interaction, f"Was {runner.card.player.name} thrown out?", custom_confirm_label="Thrown Out", custom_cancel_label="Safe", ) if is_lead_out: this_play.outs = 1 this_play.batter_final = 1 if this_play.on_third: this_play.on_first_final = ( 2 if this_play.on_first is not None else 0 ) this_play.on_second_final = ( 3 if this_play.on_second is not None else 0 ) elif this_play.on_second: this_play.on_first_final = ( 2 if this_play.on_first is not None else 0 ) else: this_play = advance_runners(session, this_play, num_bases=1) this_play.batter_final = 1 return this_play async def gb_result_12( session: Session, this_play: Play, interaction: discord.Interaction ): logger.info("GB 12") if this_play.check_pos in ["1B", "2B"]: return gb_result_3(session, this_play) elif this_play.check_pos == "3B": return gb_result_1(session, this_play) else: return await gb_decide(session, this_play, interaction) def gb_result_13(session: Session, this_play: Play): logger.info("GB 13") if this_play.check_pos in ["C", "3B"]: num_outs = 2 if this_play.starting_outs <= 1 else 1 this_play.ab, this_play.outs = 1, num_outs this_play.batter_final = 1 else: this_play = gb_result_2(session, this_play) return this_play async def new_game_conflicts(session: Session, interaction: discord.Interaction): conflict = get_channel_game_or_none(session, interaction.channel_id) if conflict is not None: await interaction.edit_original_response( content="Ope. There is already a game going on in this channel. Please wait for it to complete " "before starting a new one." ) log_exception( GameException, f"{interaction.user} attempted to start a new game in {interaction.channel.name}, but there is another active game", ) if ( interaction.channel.category is None or interaction.channel.category.name != PUBLIC_FIELDS_CATEGORY_NAME ): await interaction.edit_original_response( content="Why don't you head down to one of the Public Fields that way other humans can help if anything pops up?" ) log_exception( GameException, f"{interaction.user} attempted to start a new game in {interaction.channel.name} so they were redirected to {PUBLIC_FIELDS_CATEGORY_NAME}", ) async def select_ai_reliever(session: Session, ai_team: Team, this_play: Play) -> Card: logger.info("Selecting an AI reliever") ai_score = ( this_play.away_score if this_play.game.away_team_id == ai_team.id else this_play.home_score ) human_score = ( this_play.home_score if this_play.game.away_team_id == ai_team.id else this_play.away_score ) logger.info(f"scores - ai: {ai_score} / human: {human_score}") if abs(ai_score - human_score) >= 7: need = "length" elif this_play.inning_num >= 9 and abs(ai_score - human_score) <= 3: need = "closer" elif this_play.inning_num in [7, 8] and abs(ai_score - human_score) <= 3: need = "setup" elif abs(ai_score - human_score) <= 3: need = "middle" else: need = "length" logger.info(f"need: {need}") used_pitchers = get_game_lineups(session, this_play.game, ai_team, is_active=False) used_player_ids = [this_play.pitcher.player_id] id_string = f"&used_pitcher_ids={this_play.pitcher.player_id}" for x in used_pitchers: used_player_ids.append(x.player_id) id_string += f"&used_pitcher_ids={x.player_id}" logger.info(f"used ids: {used_player_ids}") all_links = get_game_cardset_links(session, this_play.game) if len(all_links) > 0: cardset_string = "" for x in all_links: cardset_string += f"&cardset_id={x.cardset_id}" else: cardset_string = "" logger.info(f"cardset_string: {cardset_string}") rp_json = await db_get( f"teams/{ai_team.id}/rp/{this_play.game.game_type.split('-run')[0]}?need={need}{id_string}{cardset_string}" ) rp_player = await get_player_or_none( session, player_id=get_player_id_from_dict(rp_json) ) if rp_player is None: log_exception( PlayerNotFoundException, f"Reliever not found for the {ai_team.lname}" ) logger.info(f"rp_player: {rp_player}") rp_card = await get_or_create_ai_card(session, rp_player, ai_team) logger.info(f"rp_card: {rp_card}") return rp_card def substitute_player( session, this_play: Play, old_player: Lineup, new_player: Card, position: str ) -> Lineup: logger.info( f"Substituting {new_player.player.name_with_desc} in for {old_player.card.player.name_with_desc} at {position}" ) new_lineup = Lineup( team=old_player.team, player=new_player.player, card=new_player, position=position, batting_order=old_player.batting_order, game=this_play.game, after_play=max(this_play.play_num - 1, 0), replacing_id=old_player.id, ) logger.info(f"new_lineup: {new_lineup}") session.add(new_lineup) logger.info("De-activating last player") old_player.active = False session.add(old_player) logger.info("Updating play's pitcher") this_play.pitcher = new_lineup session.add(this_play) session.commit() session.refresh(new_lineup) return new_lineup