import asyncio import copy import logging import discord from discord import SelectOption from discord.app_commands import Choice 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 DTwentyRoll, d_twenty_roll, frame_plate_check, sa_fielding_roll from exceptions import * from gauntlets import post_result from helpers import COLORS, DEFENSE_LITERAL, DEFENSE_NO_PITCHER_LITERAL, SBA_COLOR, get_channel, position_name_to_abbrev, team_role from in_game.ai_manager import get_starting_lineup 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_plays_by_pitcher, 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 in_game.managerai_responses import DefenseResponse 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(f'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 as e: logger.info(f'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 += f'\n***F A T I G U E D***' if len(baserunner_string) > 0: logger.info(f'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(f'Setting embed image to batter card') embed.set_image(url=curr_play.batter.player.batter_card_url) if len(baserunner_string) > 0: logger.info(f'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(f'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(f'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(f'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(f'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 as e: return None logger.info(f'Getting teams') away_team = await get_team_or_none(session, team_abbrev=away_team_abbrev) if away_team is None: logger.error(f'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(f'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(f'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': await get_position(session, batter.card, batter.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(f'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 except PlayNotFoundException as e: 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(f'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(f'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(f'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(f'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(f'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(f'Pitcher is not currently fatigued') if outs >= pow_outs: logger.info(f'Pitcher is beyond POW - adding fatigue') new_pitcher.is_fatigued = True elif new_pitcher.replacing_id is None: logger.info(f'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(f'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(f'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(f'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(f'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, 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, f'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.') 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) 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 as e: logger.error(f'Could not pull roster for {this_team.abbrev}: {e}') log_exception(GoogleSheetsException, f'Uh oh. Looks like your roster might not be saved. I am reading blanks when I try to get the card IDs') for x in card_ids: this_card = await get_card_or_none(session, card_id=x) 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) -> tuple[Game, Team, Play]: """ Commits this_play """ 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(f'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(f'Hm, I was not able to find a gauntlet team for you.') if not owner_team.id 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) 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 )).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 )).one() logger.info(f'errors: {errors} / outs: {outs}') if outs is not None: if errors + outs + this_play.error >= 3: logger.info(f'unearned run') is_earned = False last_ab.e_run = 1 if is_earned else 0 session.add(last_ab) session.commit() return True 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 = f'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 = f'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 = f'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(f'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(f'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 += f'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 = f'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=f'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(f'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(f'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 += f'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 = f'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 = f'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=f'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 += f' - 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(f'Lomax going in') if this_play.on_base_code <= 3 or this_play.starting_outs == 1: logger.info(f'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(f'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 = f'Potential Triple Play' ranges_embed.description = f'{this_play.pitcher.team.lname}' ranges_embed.add_field(name=f'Double Play Range', value='1 - 13') ranges_embed.add_field(name=f'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(f'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(f'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 runner_bc = 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 safe_range = None 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=f'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(f'Adding 2 for 2 outs') lead_safe_range += 2 lead_runner_embed.add_field(name='2-Out Mod', value=f'+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=f'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 += f'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 = f'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 += f'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(f'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(f'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(f'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(f'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(f'capping safe range at 19') trail_safe_range = 19 safe_string += f'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(f'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, f'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(f'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(f'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(f'Add an rbi') this_play.rbi += 1 elif this_play.batter == trail_runner: this_play.batter_final += 1 else: log_exception(LineupsMissingException, f'Could not find trail runner to advance') # Advance lead runner extra base logger.info(f'advancing lead runner') if this_play.on_second == lead_runner: logger.info(f'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(f'run scored from first') this_play.rbi += 1 log_run_scored(session, lead_runner, this_play) if trail_runner != this_play.batter: logger.info(f'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(f'Lead runner is thrown out.') this_play.outs += 1 # Lead runner is safe else: logger.info(f'Lead runner is safe.') if this_play.on_second == lead_runner: logger.info(f'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(f'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, f'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(f'AI is not advancing with lead runner') return this_play logger.info(f'Building embeds') lead_runner_embed.add_field(name='', value='', inline=False) if lead_base == 4: logger.info(f'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(f'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=f'-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(f'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, f'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(f'Runner was thrown out') # Log out on play this_play.outs += 1 # Remove trail runner logger.info(f'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(f'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, f'Could not find trail runner to advance') # Advance lead runner extra base logger.info(f'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(f'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(f'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(f'Lead runner is thrown out.') this_play.outs += 1 if this_play.on_second == lead_runner: logger.info(f'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(f'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, f'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, f'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 += f'. 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(f'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(f'No hit result was returned') return logger.info(f'new hit result: {new_hit_result}') logger.info(f'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(f'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(f'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(f'Non-rare play SPD check') runner_speed = this_play.batter.card.batterscouting.battingcard.running speed_embed = this_play.batter.team.embed speed_embed.title = f'Catcher X-Check - Speed Check' speed_embed.description = f'{this_play.batter.player.name} Speed Check' speed_embed.add_field( name=f'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(f'Result is correct') if result == 'OUT': hit_result = 'G3' else: hit_result = 'SI1' else: logger.info(f'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(f'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(f'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 = 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(f'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(f'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(f'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: print(f'1: ') 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 final_inning = this_play.inning_num if this_play.inning_half == 'bot' else this_play.inning_num - 1 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(f'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(f'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=f'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(f'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=f'Baserunning', value=run_string ) logger.info(f'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(f'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(f'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: db_game = await db_post('games', payload=game_data) except Exception as e: logger.error(f'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(f'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=f'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(f'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(f'AI on defense, playing back') to_mif = await ask_confirm( interaction, question=f'Was that ball hit to either 2B or SS?', label_type='yes' ) if to_mif or not def_alignment.corners_in: logger.info(f'playing back, gb 5') this_play = gb_result_5(session, this_play, to_mif) else: logger.info(f'corners in, gb 7') this_play = gb_result_7(session, this_play) else: logger.info(f'Checking if hit to MIF') to_mif = await ask_confirm( interaction, question=f'Was that ball hit to either 2B or SS?', label_type='yes' ) if to_mif: playing_in = await ask_confirm( interaction, question=f'Were they playing in?', label_type='yes', ) if playing_in: logger.info(f'To MIF, batter out, runners hold') this_play = gb_result_1(session, this_play) else: logger.info(f'To MIF, playing back, gb 3') this_play = gb_result_3(session, this_play) else: logger.info(f'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(f'playing in, gb 7') this_play = gb_result_7(session, this_play) else: logger.info(f'playing in, gb 10') this_play = gb_result_10(session, this_play) elif def_alignment.corners_in: logger.info(f'Checking if ball was hit to 1B/3B') to_cif = await ask_confirm( interaction, f'Was that ball hit to 1B/3B?', label_type='yes' ) if to_cif: if this_play.on_base_code == 5: logger.info(f'Corners in, gb 7') this_play = gb_result_7(session, this_play) else: logger.info(f'Corners in, gb 10') this_play = gb_result_10(session, this_play) else: logger.info(f'Corners back, gb 2') this_play = gb_result_2(session, this_play) else: logger.info(f'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(f'playing in, gb 7') this_play = gb_result_7(session, this_play) elif playing_in: logger.info(f'playing in, gb 10') this_play = gb_result_10(session, this_play) else: logger.info(f'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(f'playing in, gb 11') this_play = gb_result_11(session, this_play) elif playing_in: logger.info(f'playing in, gb 9') this_play = gb_result_9(session, this_play) else: logger.info(f'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(f'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(f'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(f'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(f'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(f'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(f'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(f'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(f'GB 8') return gb_result_7(session, this_play) def gb_result_9(session: Session, this_play: Play): logger.info(f'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(f'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(f'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(f'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(f'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(f'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=f'Ope. There is already a game going on in this channel. Please wait for it to complete ' f'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=f'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(f'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(f'De-activating last player') old_player.active = False session.add(old_player) logger.info(f'Updating play\'s pitcher') this_play.pitcher = new_lineup session.add(this_play) session.commit() session.refresh(new_lineup) return new_lineup