From 90d73458503ab8b87f66e82156daee1f0ae276f0 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Tue, 3 Feb 2026 23:13:40 -0600 Subject: [PATCH] Implement play locking to prevent concurrent command processing Adds idempotency guard to prevent race conditions when multiple users submit commands for the same play simultaneously. Changes: - Add PlayLockedException for locked play detection - Implement lock check in checks_log_interaction() - Acquire lock (play.locked = True) before processing commands - Release lock (play.locked = False) after play completion - Add warning logs for rejected duplicate submissions - Add /diagnostics endpoint to health server for debugging This prevents database corruption and duplicate processing when users spam commands like "log xcheck" while the first is still processing. Tested successfully in Discord - duplicate commands now properly return PlayLockedException with instructions to wait. Co-Authored-By: Claude Sonnet 4.5 --- command_logic/logic_gameplay.py | 4146 +++++++++++++++++++------------ exceptions.py | 11 + health_server.py | 106 +- 3 files changed, 2580 insertions(+), 1683 deletions(-) diff --git a/command_logic/logic_gameplay.py b/command_logic/logic_gameplay.py index e6e72de..7e9dfe6 100644 --- a/command_logic/logic_gameplay.py +++ b/command_logic/logic_gameplay.py @@ -1,4 +1,3 @@ - import asyncio import copy import logging @@ -14,28 +13,80 @@ 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 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.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.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' -} +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: +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. @@ -55,90 +106,88 @@ def safe_wpa_lookup(inning_half: str, inning_num: int, starting_outs: int, on_ba 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' + 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']) + return float(WPA_DF.loc[key, "home_win_ex"]) except KeyError: - logger.warning(f'WPA key not found: {key}, attempting fallback') + 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' + 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}') + 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}') + 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}') + 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}') +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}' + 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') + 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') + 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}') + 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'] + embed_color = COLORS["red"] elif curr_play.pitcher.after_play == curr_play.play_num - 1: - embed_color = COLORS['white'] + embed_color = COLORS["white"] elif curr_play.is_new_inning: - embed_color = COLORS['yellow'] + embed_color = COLORS["yellow"] else: - embed_color = COLORS['sba'] + embed_color = COLORS["sba"] embed.color = embed_color def steal_string(batting_card: BattingCard) -> str: - steal_string = '-/- (---)' + steal_string = "-/- (---)" if batting_card.steal_jump > 0: jump_chances = round(batting_card.steal_jump * 36) @@ -155,125 +204,130 @@ async def get_scorebug_embed(session: Session, this_game: Game, full_length: boo elif jump_chances == 1: good_jump = 2 elif jump_chances == 7: - good_jump = '4,5' + good_jump = "4,5" elif jump_chances == 8: - good_jump = '4,6' + good_jump = "4,6" elif jump_chances == 9: - good_jump = '3-5' + good_jump = "3-5" elif jump_chances == 10: - good_jump = '2-5' + good_jump = "2-5" elif jump_chances == 11: - good_jump = '6,7' + good_jump = "6,7" elif jump_chances == 12: - good_jump = '4-6' + good_jump = "4-6" elif jump_chances == 13: - good_jump = '2,4-6' + good_jump = "2,4-6" elif jump_chances == 14: - good_jump = '3-6' + good_jump = "3-6" elif jump_chances == 15: - good_jump = '2-6' + good_jump = "2-6" elif jump_chances == 16: - good_jump = '2,5-6' + good_jump = "2,5-6" elif jump_chances == 17: - good_jump = '3,5-6' + good_jump = "3,5-6" elif jump_chances == 18: - good_jump = '4-6' + good_jump = "4-6" elif jump_chances == 19: - good_jump = '2,4-7' + good_jump = "2,4-7" elif jump_chances == 20: - good_jump = '3-7' + good_jump = "3-7" elif jump_chances == 21: - good_jump = '2-7' + good_jump = "2-7" elif jump_chances == 22: - good_jump = '2-7,12' + good_jump = "2-7,12" elif jump_chances == 23: - good_jump = '2-7,11' + good_jump = "2-7,11" elif jump_chances == 24: - good_jump = '2,4-8' + good_jump = "2,4-8" elif jump_chances == 25: - good_jump = '3-8' + good_jump = "3-8" elif jump_chances == 26: - good_jump = '2-8' + good_jump = "2-8" elif jump_chances == 27: - good_jump = '2-8,12' + good_jump = "2-8,12" elif jump_chances == 28: - good_jump = '2-8,11' + good_jump = "2-8,11" elif jump_chances == 29: - good_jump = '3-9' + good_jump = "3-9" elif jump_chances == 30: - good_jump = '2-9' + good_jump = "2-9" elif jump_chances == 31: - good_jump = '2-9,12' + good_jump = "2-9,12" elif jump_chances == 32: - good_jump = '2-9,11' + good_jump = "2-9,11" elif jump_chances == 33: - good_jump = '2-10' + good_jump = "2-10" elif jump_chances == 34: - good_jump = '3-11' + good_jump = "3-11" elif jump_chances == 35: - good_jump = '2-11' + 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})' + 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 = '' + + 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' + 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' + 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}') + 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')}' + 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***' + 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') + 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}' + 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 - ) + pit_string += f"\n{get_pitching_statline(session, curr_play.pitcher)}" + embed.add_field(name="Pitcher", value=pit_string) - logger.info(f'Setting embed image to batter card') + 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() + 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}') + 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) - 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') + 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]: @@ -281,144 +335,206 @@ async def get_scorebug_embed(session: Session, this_game: Game, full_length: boo else: to_base = 4 - jump_resp = curr_play.managerai.check_jump(session, this_game, to_base=to_base) + 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') + logger.info(f"Checking defensive alignment") def_align = curr_play.managerai.defense_alignment(session, this_game) - logger.info(f'def_align: {def_align}') + 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}') + 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) + 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) - + 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) + 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) + 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') + 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) + name=f"{this_game.away_team.abbrev} Lineup", + value=this_game.team_lineup(session, this_game.away_team), ) - logger.info(f'Pulling home lineup') + 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) + 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): +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}') + 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') + 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') + 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!' + 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') + 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!' + 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') + 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}') + 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}' + 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.' + 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 - } + 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}') +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') + 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 + 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): +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}') + logger.info(f"sorted pitchers: {pitchers}") if len(pitchers) == 0: - log_exception(MissingRosterException, 'No pitchers were found to select RP') + 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 + 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): +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] + 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') - + 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() - + 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.') + log_exception( + PlayerNotFoundException, + f"I dont see any legal defenders for {new_position} on the field.", + ) defender_selection = SelectDefensiveChange( this_game=this_game, @@ -426,64 +542,97 @@ async def defender_dropdown_view(session: Session, this_game: Game, 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] + 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]): +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}') + 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 + 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): +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 + 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.' + 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...') + + 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...') + 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': + 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: +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: @@ -504,10 +653,12 @@ def get_obc(on_first = None, on_second = None, on_third = None) -> int: else: obc = 0 - return obc + return obc -def get_re24(this_play: Play, runs_scored: int, new_obc: int, new_starting_outs: int) -> float: +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], @@ -516,11 +667,15 @@ def get_re24(this_play: Play, runs_scored: int, new_obc: int, new_starting_outs: 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] + 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] + 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) @@ -541,208 +696,299 @@ def get_wpa(this_play: Play, next_play: Play): 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): + 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) + 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) + 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': + if this_play.inning_half == "top": return wpa * -1.0 return wpa -def complete_play(session:Session, this_play: Play): +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}') + 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') + 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' + 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) + 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') + 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') + 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) + (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') + 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') + 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') + 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') + 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') + 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') + 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') + 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 + 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') + 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) + (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') + 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') + 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') + 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') + 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') + 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') + 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') + 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': + + 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: + + 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) + 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_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( + 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() + ) + ).one() if outs is None: outs = 0 - logger.info(f'Outs recorded: {outs}') - + 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}') + 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}') - + logger.info(f"Using relief rating, POW outs: {pow_outs}") + if not new_pitcher.is_fatigued: - logger.info(f'Pitcher is not currently fatigued') + logger.info(f"Pitcher is not currently fatigued") if outs >= pow_outs: - logger.info(f'Pitcher is beyond POW - adding fatigue') + 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}') + 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') + 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}') + 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') + 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}') + 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') + 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}') + 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, + game=this_play.game, play_num=this_play.play_num + 1, batting_order=nbo, inning_half=nih, @@ -754,7 +1000,7 @@ def complete_play(session:Session, this_play: Play): batter=new_batter, batter_pos=new_batter.position, pitcher=new_pitcher, - catcher=get_one_lineup(session, this_play.game, new_pitcher_team, position='C'), + 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, @@ -762,15 +1008,15 @@ def complete_play(session:Session, this_play: Play): 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) + 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}') + logger.info(f"this_play: {this_play}") + logger.info(f"new_play: {new_play}") session.add(this_play) session.add(new_play) @@ -780,16 +1026,23 @@ def complete_play(session:Session, this_play: 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}') +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}') + 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}') + 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 @@ -797,44 +1050,51 @@ async def get_lineups_from_sheets(session: Session, sheets, this_game: Game, thi row_start = 18 row_end = 26 - logger.info(f'roster_num: {roster_num}') + logger.info(f"roster_num: {roster_num}") if int(roster_num) == 1: - l_range = f'H{row_start}:I{row_end}' + l_range = f"H{row_start}:I{row_end}" elif int(roster_num) == 2: - l_range = f'J{row_start}:K{row_end}' + l_range = f"J{row_start}:K{row_end}" else: - l_range = f'L{row_start}:M{row_end}' + l_range = f"L{row_start}:M{row_end}" - logger.info(f'l_range: {l_range}') + logger.info(f"l_range: {l_range}") raw_cells = r_sheet.range(l_range) - logger.info(f'raw_cells: {raw_cells}') + 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}') + 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') - + 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: + 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.') + 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?' + 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.') + 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)) @@ -844,136 +1104,202 @@ async def get_lineups_from_sheets(session: Session, sheets, this_game: Game, thi game=this_game, team=this_team, player=this_card.player, - card=this_card + 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"]}') + 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.') + 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]: +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}') + 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}') + logger.debug(f"this_sheet: {this_sheet}") - r_sheet = this_sheet.worksheet_by_title('My Rosters') - logger.debug(f'r_sheet: {r_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' + l_range = "B3:B28" elif roster_num == 2: - l_range = 'B29:B54' + l_range = "B29:B54" else: - l_range = 'B55:B80' + 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}') + 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}') + logger.info(f"raw_cells: {raw_cells}") - await roster_message.edit(content='Got your roster, now to find these cards in your collection...') + 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}') + logger.info(f"card_ids: {card_ids}") except (ValueError, TypeError) as e: - logger.error(f'Could not pull roster for {this_team.abbrev}: {e}') - logger.error(f'raw_cells type: {type(raw_cells)} / raw_cells: {raw_cells}') - raise GoogleSheetsException(f'Uh oh. I had trouble reading your roster from Google Sheets (range {l_range}). Please make sure your roster is saved properly and all cells contain valid card IDs.') + logger.error(f"Could not pull roster for {this_team.abbrev}: {e}") + logger.error(f"raw_cells type: {type(raw_cells)} / raw_cells: {raw_cells}") + raise GoogleSheetsException( + f"Uh oh. I had trouble reading your roster from Google Sheets (range {l_range}). Please make sure your roster is saved properly and all cells contain valid card IDs." + ) for x in card_ids: this_card = await get_card_or_none(session, card_id=x) if this_card is None: - logger.error(f'Card ID {x} not found while loading roster for team {this_team.abbrev}') - raise CardNotFoundException(f'Card ID {x} was not found in your collection. Please check your roster sheet and make sure all card IDs are valid.') - session.add(RosterLink( - game=this_game, - card=this_card, - team=this_team - )) - + logger.error( + f"Card ID {x} not found while loading roster for team {this_team.abbrev}" + ) + raise CardNotFoundException( + f"Card ID {x} was not found in your collection. Please check your roster sheet and make sure all card IDs are valid." + ) + session.add(RosterLink(game=this_game, card=this_card, team=this_team)) + session.commit() - await roster_message.edit(content='Your roster is logged and scouting data is available.') - return session.exec(select(RosterLink).where(RosterLink.game == this_game, RosterLink.team == this_team)).all() + 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]: +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}') + 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.') - + 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}' + 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.') - + 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.') + 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') - + 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') + logger.error( + f"{command_name} command: No play found for Game ID {this_game.id} - attempting to initialize play" + ) this_play = activate_last_play(session, this_game) + if this_play.locked: + logger.warning( + f"{interaction.user.name} attempted {command_name} on locked play {this_play.id} " + f"in game {this_game.id}. Rejecting duplicate submission." + ) + from exceptions import PlayLockedException + + raise PlayLockedException( + "This play is already being processed. Please wait for the current action to complete.\n\n" + "If this play appears stuck, wait 30 seconds and try again (auto-unlock will trigger)." + ) + 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): +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}') + 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}') + 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}") - 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') + logger.info(f"unearned run") is_earned = False last_ab.e_run = 1 if is_earned else 0 @@ -982,7 +1308,9 @@ def log_run_scored(session: Session, runner: Lineup, this_play: Play, is_earned: return True -def create_pinch_runner_entry_play(session: Session, game: Game, current_play: Play, pinch_runner_lineup: Lineup) -> Play: +def create_pinch_runner_entry_play( + session: Session, game: Game, current_play: Play, pinch_runner_lineup: Lineup +) -> Play: """ Creates a "pinch runner entry" Play record when a pinch runner substitutes for a player on base. This Play has PA=0, AB=0 so it doesn't affect counting stats, but provides a record to mark @@ -999,10 +1327,14 @@ def create_pinch_runner_entry_play(session: Session, game: Game, current_play: P Returns: The newly created entry Play """ - logger.info(f'Creating pinch runner entry Play for {pinch_runner_lineup.player.name_with_desc} in Game {game.id}') + logger.info( + f"Creating pinch runner entry Play for {pinch_runner_lineup.player.name_with_desc} in Game {game.id}" + ) # Get the next play number - max_play_num = session.exec(select(func.max(Play.play_num)).where(Play.game == game)).one() + max_play_num = session.exec( + select(func.max(Play.play_num)).where(Play.game == game) + ).one() next_play_num = max_play_num + 1 if max_play_num else 1 # Create the entry Play @@ -1029,29 +1361,37 @@ def create_pinch_runner_entry_play(session: Session, game: Game, current_play: P is_tied=current_play.is_tied, is_go_ahead=False, is_new_inning=False, - managerai_id=current_play.managerai_id + managerai_id=current_play.managerai_id, ) session.add(entry_play) session.commit() session.refresh(entry_play) - logger.info(f'Created entry Play #{entry_play.play_num} for pinch runner {pinch_runner_lineup.player.name_with_desc}') + logger.info( + f"Created entry Play #{entry_play.play_num} for pinch runner {pinch_runner_lineup.player.name_with_desc}" + ) return entry_play -def advance_runners(session: Session, this_play: Play, num_bases: int, only_forced: bool = False, earned_bases: int = None) -> Play: +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}') + 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 + 1: True if earned_bases >= 3 else False, } if num_bases == 0: @@ -1061,24 +1401,28 @@ def advance_runners(session: Session, this_play: Play, num_bases: int, only_forc 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]) + 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]) + 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 @@ -1103,7 +1447,9 @@ def advance_runners(session: Session, this_play: Play, num_bases: int, only_forc 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]) + 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 @@ -1111,7 +1457,9 @@ def advance_runners(session: Session, this_play: Play, num_bases: int, only_forc 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]) + 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 @@ -1121,7 +1469,9 @@ def advance_runners(session: Session, this_play: Play, num_bases: int, only_forc 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]) + 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 @@ -1129,49 +1479,68 @@ def advance_runners(session: Session, this_play: Play, num_bases: int, only_forc 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') +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}') + 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.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, + 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 + author_icon=this_team.logo, ) - view.cancel_button.label = f'Center Fielder' + 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, + 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 + author_icon=this_team.logo, ) - view.right_button.label = f'Right Fielder' + 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, + 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 + author_icon=this_team.logo, ) page_num = 1 @@ -1182,11 +1551,11 @@ async def show_outfield_cards(session: Session, interaction: discord.Interaction await view.wait() if view.value: - if view.value == 'left': + if view.value == "left": page_num = 0 - if view.value == 'cancel': + if view.value == "cancel": page_num = 1 - if view.value == 'right': + if view.value == "right": page_num = 2 else: await msg.edit(content=None, embed=embeds[page_num], view=None) @@ -1214,25 +1583,30 @@ async def show_outfield_cards(session: Session, interaction: discord.Interaction 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: +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': + 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': + 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.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: @@ -1242,241 +1616,258 @@ async def flyballs(session: Session, interaction: discord.Interaction, this_play 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') + 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': + if this_of.position == "LF": of_mod = -2 - elif this_of.position == 'RF': + elif this_of.position == "RF": of_mod = 2 - logger.debug(f'done with of embed') + 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 + + 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 ""}' + safe_string = f"1{' - ' if max_safe > 1 else ''}" if max_safe > 1: if max_safe <= 20: - safe_string += f'{max_safe}' + safe_string += f"{max_safe}" else: - safe_string += f'20' + safe_string += f"20" if min_out > 20: - out_string = 'None' + out_string = "None" else: - out_string = f'{min_out}{" - 20" if min_out < 20 else ""}' + out_string = f"{min_out}{' - 20' if min_out < 20 else ''}" - hold_string = '' + hold_string = "" if max_safe != min_out: - hold_string += f'{min_hold}' + hold_string += f"{min_hold}" if min_out - 1 > min_hold: - hold_string += f' - {min_out - 1}' + 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 + 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}') + 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!' + content=f"**{runner.name}** is tagging from second!" ) else: await interaction.channel.send( - content=f'**{runner.name}** is holding at second.' + 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', + 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' + result = "out" elif this_roll.d_twenty <= max_safe: - result = 'safe' + result = "safe" else: - result = 'holds' - + 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 + 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] + 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 + 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' + if view.value == "Tagged Up": + result = "safe" + elif view.value == "Out at 3rd": + result = "out" else: - result = 'holds' + result = "holds" else: await question.delete() - - if result == 'safe': + + if result == "safe": this_play.on_second_final = 3 - elif result == 'out': + elif result == "out": num_outs += 1 this_play.on_second_final = None this_play.outs = num_outs - elif flyball_type == 'b?': + 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') + 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') + 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 + max_safe = ( + runner_lineup.card.batterscouting.battingcard.running + of_rating.arm + ) - safe_string = f'1{" - " if max_safe > 1 else ""}' + safe_string = f"1{' - ' if max_safe > 1 else ''}" if max_safe > 1: if max_safe <= 20: - safe_string += f'{max_safe - 1}' + safe_string += f"{max_safe - 1}" else: - safe_string += f'20' - + safe_string += f"20" + if max_safe == 20: - out_string = 'None' - catcher_string = '20' + out_string = "None" + catcher_string = "20" elif max_safe > 20: - out_string = 'None' - catcher_string = 'None' + out_string = "None" + catcher_string = "None" elif max_safe == 19: - out_string = 'None' - catcher_string = '19 - 20' + out_string = "None" + catcher_string = "19 - 20" elif max_safe == 18: - out_string = f'20' - catcher_string = '18 - 19' + out_string = f"20" + catcher_string = "18 - 19" else: - out_string = f'{max_safe + 2} - 20' - catcher_string = f'{max_safe} - {max_safe + 1}' - + 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 + 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}') + 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!' + content=f"**{runner.name}** is tagging from third!" ) else: await interaction.channel.send( - content=f'**{runner.name}** is holding at third.' + 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', + 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!' + 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!' + 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!' - + 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 + 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' + question=f"Was {runner.name} thrown out?", + label_type="yes", + ) + + elif result == "catcher": + catcher_rating = await get_position( + session, this_play.catcher.card, "C" ) - - 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.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 + name="Catcher Range", value=catcher_rating.range ) - runner_embed.add_field(name='', value='', inline=False) - + runner_embed.add_field(name="", value="", inline=False) + if catcher_rating.range == 1: safe_range = 3 elif catcher_rating.range == 2: @@ -1488,42 +1879,39 @@ async def flyballs(session: Session, interaction: discord.Interaction, this_play 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}' + 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 - ) + 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, - embed=runner_embed + content=None, embeds=this_roll.embeds ) - 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]}') + 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]}!' + 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]}') + 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]}!' - + 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' + 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 + 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 @@ -1535,30 +1923,39 @@ async def flyballs(session: Session, interaction: discord.Interaction, this_play this_play.on_third_final = 4 log_run_scored(session, this_play.on_third, this_play) - elif flyball_type == 'c': + 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: +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.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 ( + 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') + logger.info(f"Lead runner is out") this_play.outs = 2 if this_play.on_third is not None: @@ -1566,368 +1963,423 @@ async def lineouts(session: Session, interaction: discord.Interaction, this_play 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') + 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') + 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] + 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!') + 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!') + 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?' + 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') + 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') + 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') + 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): +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}') + logger.info(f"this_roll: {this_roll}") + + await interaction.edit_original_response(content=None, embeds=this_roll.embeds) - 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') + 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): +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}') + 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') + 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}') + 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}') + 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') + 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 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}') + 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}') + 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' + logger.info(f"trail runner batting card: {trail_bc}") - 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_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}') + 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}") - 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') - + 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 ""}' + safe_string = f"1{' - ' if safe_range > 1 else ''}" if safe_range > 1: if safe_range <= 20: - safe_string += f'{safe_range - 1}' + safe_string += f"{safe_range - 1}" else: - safe_string += f'20' + safe_string += f"20" if safe_range == 20: - out_string = 'None' - catcher_string = '20' + out_string = "None" + catcher_string = "20" elif safe_range > 20: - out_string = 'None' - catcher_string = 'None' + out_string = "None" + catcher_string = "None" elif safe_range == 19: - out_string = 'None' - catcher_string = '19 - 20' + out_string = "None" + catcher_string = "19 - 20" elif safe_range == 18: - out_string = f'20' - catcher_string = '18 - 19' + 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}') + 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} + 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 ""}' + safe_string = f"1{' - ' if safe_range > 1 else ''}" if safe_range > 1: if safe_range <= 20: - safe_string += f'{safe_range}' + safe_string += f"{safe_range}" else: - safe_string += f'20' + safe_string += f"20" if safe_range > 19: - out_string = '20' + 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} + 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}') + 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 + 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') + 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]}') + 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]}!' + 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]}') + 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]}!' - + 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' + 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}') + 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', + 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}') + 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]}') + 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]}!' + 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]}') + 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]}!' - + 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 + 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') + 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]}') + 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]}!' + 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]}') + 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]}!' - + 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 + 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') + 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): + 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') + 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}') + 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="Runner Not Held", value="+1") + + lead_runner_embed.add_field(name="", value="", inline=False) - lead_runner_embed.add_field(name='', value='', inline=False) - if lead_base == 4: - logger.info(f'lead base is 4, building strings') + 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']) - + 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') + 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) + lead_runner_embed.add_field(name="Safe Range", value=lead_strings["safe"]) + lead_runner_embed.add_field(name="Out Range", value=lead_strings["out"]) - logger.info(f'Building strings for trail runner') - safe_string = f'1{" - " if trail_safe_range > 1 else ""}' + # 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}' + safe_string += f"{trail_safe_range}" else: - logger.info(f'capping safe range at 19') + logger.info(f"capping safe range at 19") trail_safe_range = 19 - safe_string += f'19' + safe_string += f"19" - out_string = f'{trail_safe_range + 1} - 20' - logger.info(f'safe: {safe_string} / out: {out_string}') + 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) + 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' + question=f"Is **{lead_runner.player.name}** being sent {TO_BASE[lead_base]}?", + label_type="yes", ) if is_lead_running: @@ -1935,10 +2387,12 @@ async def check_uncapped_advance(session: Session, interaction: discord.Interact if this_game.ai_team: throw_resp = this_play.managerai.throw_at_uncapped(session, this_game) - logger.info(f'throw_resp: {throw_resp}') - + 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]}') + 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 @@ -1948,15 +2402,17 @@ async def check_uncapped_advance(session: Session, interaction: discord.Interact await asyncio.sleep(1) return this_play - + else: - await interaction.channel.send(content=f'**{outfielder.player.name}** is throwing {TO_BASE[lead_base]}!') + 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' + 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 @@ -1967,53 +2423,73 @@ async def check_uncapped_advance(session: Session, interaction: discord.Interact 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' + 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]}!') + 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]}!') + 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' + 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 + 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') + 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 + this_play.on_first_final += 1 elif this_play.batter == trail_runner: - this_play.batter_final += 1 + this_play.batter_final += 1 else: - log_exception(LineupsMissingException, f'Could not find trail runner to advance') + log_exception( + LineupsMissingException, + f"Could not find trail runner to advance", + ) # Throw is going to trail runner else: @@ -2022,12 +2498,16 @@ async def check_uncapped_advance(session: Session, interaction: discord.Interact 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) + 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') + logger.info(f"logging one one additional out for trail runner") # Log out on play this_play.outs += 1 @@ -2037,53 +2517,60 @@ async def check_uncapped_advance(session: Session, interaction: discord.Interact else: this_play.batter_final = None else: - logger.info(f'Runner is safe') + 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') + 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') - + log_exception( + LineupsMissingException, + f"Could not find trail runner to advance", + ) + # Advance lead runner extra base - logger.info(f'advancing lead runner') + logger.info(f"advancing lead runner") if this_play.on_second == lead_runner: - logger.info(f'run scored from second') + 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') + 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') + 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) + 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.') + logger.info(f"Lead runner is thrown out.") this_play.outs += 1 # Lead runner is safe else: - logger.info(f'Lead runner is safe.') - + logger.info(f"Lead runner is safe.") + if this_play.on_second == lead_runner: - logger.info(f'setting lead runner on_second_final') + logger.info(f"setting lead runner on_second_final") if runner_thrown_out: this_play.on_second_final = None else: @@ -2092,7 +2579,7 @@ async def check_uncapped_advance(session: Session, interaction: discord.Interact 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') + logger.info(f"setting lead runner on_first") if runner_thrown_out: this_play.on_first_final = None @@ -2101,84 +2588,95 @@ async def check_uncapped_advance(session: Session, interaction: discord.Interact 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') - + 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) + 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' + 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}') + 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="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) - lead_runner_embed.add_field(name='', value='', inline=False) - if lead_base == 4: - logger.info(f'lead base is 4, building strings') + 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']) - + 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') + 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']) - + 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' + 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}') + 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_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}') + 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_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}') + 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']) + 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' + 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') + 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 @@ -2197,42 +2695,47 @@ async def check_uncapped_advance(session: Session, interaction: discord.Interact # 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!', + 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' + 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 + this_play.on_first_final += 1 elif this_play.batter == trail_runner: - this_play.batter_final += 1 + this_play.batter_final += 1 else: - log_exception(LineupsMissingException, f'Could not find trail runner to advance') + 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) + 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') + logger.info(f"Runner was thrown out") # Log out on play this_play.outs += 1 # Remove trail runner - logger.info(f'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') + 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: @@ -2240,35 +2743,43 @@ async def check_uncapped_advance(session: Session, interaction: discord.Interact elif this_play.batter == trail_runner: this_play.batter_final += 1 else: - log_exception(LineupsMissingException, f'Could not find trail runner to advance') + log_exception( + LineupsMissingException, + f"Could not find trail runner to advance", + ) - # Advance lead runner extra base - logger.info(f'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') + 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]}.') + 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') + 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 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]}?', @@ -2277,11 +2788,11 @@ async def check_uncapped_advance(session: Session, interaction: discord.Interact # Lead runner is thrown out if runner_thrown_out: - logger.info(f'Lead runner is 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') + logger.info(f"setting lead runner on_second_final") if runner_thrown_out: this_play.on_second_final = None @@ -2290,7 +2801,7 @@ async def check_uncapped_advance(session: Session, interaction: discord.Interact 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') + logger.info(f"setting lead runner on_first_final") if runner_thrown_out: this_play.on_first_final = None @@ -2299,25 +2810,33 @@ async def check_uncapped_advance(session: Session, interaction: discord.Interact 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') - + 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: +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 == '**': + + if single_type == "**": this_play = advance_runners(session, this_play, num_bases=2) - elif single_type in ['*', 'ballpark']: + 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 + this_play.bp1b = 1 if single_type == "ballpark" else 0 - elif single_type == 'uncapped': + 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]: @@ -2328,47 +2847,68 @@ async def singles(session: Session, interaction: discord.Interaction, this_play: 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) + 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: +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 == '**': + if double_type == "**": this_play = advance_runners(session, this_play, num_bases=2) - - elif double_type == '***': + + elif double_type == "***": this_play = advance_runners(session, this_play, num_bases=3) - - elif double_type == 'uncapped': + + 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) + 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 @@ -2382,41 +2922,53 @@ async def triples(session: Session, interaction: discord.Interaction, this_play: 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']): +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.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'): +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.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): +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 @@ -2427,58 +2979,65 @@ async def popouts(session: Session, interaction: discord.Interaction, this_play: 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): +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 +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': + if bunt_type == "sacrifice": this_play = advance_runners(session, this_play, num_bases=1) - elif bunt_type == 'popout': + elif bunt_type == "popout": this_play = advance_runners(session, this_play, num_bases=0) - elif bunt_type == 'bad': + 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': + 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': + elif bunt_type == "defense": if this_play.on_third is not None: runner = this_play.on_third lead_base = 4 @@ -2486,60 +3045,72 @@ async def bunts(session: Session, interaction: discord.Interaction, this_play: P 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]}' + 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] + 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 + 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': + if view.value == "Pitcher": defender = this_play.pitcher - elif view.value == 'Catcher': + 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') + 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.') - + 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.') + 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]}' + 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: @@ -2552,44 +3123,49 @@ async def bunts(session: Session, interaction: discord.Interaction, this_play: P 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') + 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']): +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': + 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': + + 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': + + elif chaos_type == "balk": this_play = advance_runners(session, this_play, 1) this_play.rbi = 0 this_play.balk = 1 - - elif chaos_type == 'pickoff': + + elif chaos_type == "pickoff": this_play = advance_runners(session, this_play, 0) this_play.pick_off = 1 this_play.outs = 1 @@ -2600,21 +3176,27 @@ async def chaos(session: Session, interaction: discord.Interaction, this_play: P 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: +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']: + 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 + 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 @@ -2623,34 +3205,36 @@ async def steals(session: Session, interaction: discord.Interaction, this_play: if this_play.on_second: this_play.on_second_final = 3 - if steal_type == 'steal-plus-overthrow': + 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) + 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 - + 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': + + 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 + this_play.on_first_final = 2 if steal_type == "stolen-base" else 3 - if steal_type == 'steal-plus-overthrow' and this_play.on_third: + 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': + + elif steal_type == "caught-stealing": this_play.outs = 1 if to_base == 4 and this_play.on_third: @@ -2661,33 +3245,35 @@ async def steals(session: Session, interaction: discord.Interaction, this_play: 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: +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 + session, this_play.game, this_team=defense_team, position=position ) this_play.defender = this_defender this_play.check_pos = position @@ -2696,49 +3282,48 @@ async def xchecks(session: Session, interaction: discord.Interaction, this_play: 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.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}') + 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}') + 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' + 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}') + logger.info(f"this_roll: {this_roll}") if not debug: - question = f'Looks like this is a **{this_roll.hit_result}**' + question = f"Looks like this is a **{this_roll.hit_result}**" if this_roll.is_chaos: - question += ' **rare play**' + 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?' + 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] + content=None, embeds=[defender_embed, *this_roll.embeds] ) is_correct = await ask_confirm( interaction, question, - label_type='yes', + label_type="yes", timeout=30, ) else: @@ -2748,144 +3333,149 @@ async def xchecks(session: Session, interaction: discord.Interaction, this_play: 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}') + 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') + 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' + 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'] + 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}') + 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'] + 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}') - + 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?' + question=f"Which result did **{this_defender.player.name}** allow?", ) if new_hit_result is None: - logger.error(f'No hit result was returned') + 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') + 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' + 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'] + 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}') + 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?' + 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') + logger.error(f"No error result was returned") return else: - if '1' in new_error_result: + if "1" in new_error_result: new_error_result = 1 - elif '2' in new_error_result: + 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"new_error_result: {new_error_result}") - logger.info(f'Setting hit and error results and continuing with processing.') + 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') + 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.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') + 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' + result = "SAFE" else: - result = 'OUT' - logger.info(f'SPD check roll: {this_roll.d_twenty} / runner_speed: {runner_speed} / result: {result}') + 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] + 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' + 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' + logger.info(f"Result is correct") + if result == "OUT": + hit_result = "G3" else: - hit_result = 'SI1' + hit_result = "SI1" else: - logger.info(f'Result is NOT correct') - if result == 'OUT': - hit_result = 'SI1' + 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') + 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' + 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: + 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' + 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, @@ -2893,114 +3483,181 @@ async def xchecks(session: Session, interaction: discord.Interaction, this_play: # 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')): + 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' + 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' + 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')): + + 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' + 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' + 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' + 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, '*') + 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, '**') + + 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, '***') + + 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': + + 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': + + 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) + + 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': + + 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) + 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': + 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) - + 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': + 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, '*') + this_play = await singles(session, interaction, this_play, "*") else: - this_play = await gb_letter(session, interaction, this_play, 'C', position=this_play.check_pos, defender_is_in=playing_in) - - elif hit_result == 'SPD': - this_play = await singles(session, interaction, this_play, '*') - - elif hit_result == 'F1': + this_play = await gb_letter( + session, + interaction, + this_play, + "C", + position=this_play.check_pos, + defender_is_in=playing_in, + ) + + elif hit_result == "SPD": + this_play = await singles(session, interaction, this_play, "*") + + elif hit_result == "F1": this_play.outs = 1 this_play.ab = 1 if this_play.on_third is None else 0 if this_play.on_base_code > 0 and this_play.starting_outs < 2: this_play = advance_runners(session, this_play, 1) - + if this_play.on_second is not None: this_play.on_second_final = 4 - log_run_scored(session, this_play.on_second, this_play, is_earned=False) - + 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': + + 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 @@ -3008,105 +3665,121 @@ async def xchecks(session: Session, interaction: discord.Interaction, this_play: 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') + 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: + 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) - + 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) - + 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) - + 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: + + 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) - + 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: + + 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') - + + 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') + 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': + 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 @@ -3114,51 +3787,67 @@ async def xchecks(session: Session, interaction: discord.Interaction, this_play: 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 = 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 = 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: + + 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.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}') + 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 @@ -3167,72 +3856,97 @@ def activate_last_play(session: Session, this_game: Game) -> 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() + + 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)]: + 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}') + 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}') + 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') + 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}') + 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}...') + 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}') + logger.info(f"Initialized play: {this_play.id}") except PlayInitException: - logger.info(f'Plays found, attempting to active the last play') + 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}') + 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): +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' + "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 + 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) + + 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, + 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 + author_icon=this_play.pitcher.team.logo, ) player_dropdown = SelectViewDefense( options=select_player_options, @@ -3240,285 +3954,323 @@ async def show_defense_cards(session: Session, interaction: discord.Interaction, base_embed=player_embed, session=session, sorted_lineups=sorted_lineups, - responders=[interaction.user] + 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) + await interaction.edit_original_response( + content=None, embed=player_embed, view=dropdown_view + ) def is_game_over(this_play: Play) -> bool: - if this_play.inning_num < 9 and (abs(this_play.away_score - this_play.home_score) < 10): + 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': + 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': + + 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: + + 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: + + 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)]) +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 + 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.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}' + 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}') + 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' + 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' + 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_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 + 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' + 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_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 - ) + 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"] - 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"]}' + 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_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 - ) + 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' + 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)}' + 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}' + hold_string = f"Holds: {player_name}" else: - hold_string += f', {player_name}' + hold_string += f", {player_name}" if hold_string is not None: - pit_string += f'{hold_string}\n' + 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}' + 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', + name=f"Pitching", value=pit_string, ) def name_list(raw_list: list) -> str: - logger.info(f'raw_list: {raw_list}') + 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 + 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 + if x["player_id"] not in data_dict: + data_dict[x["player_id"]] = 1 else: - data_dict[x['player_id']] += 1 + data_dict[x["player_id"]] += 1 - r_string = '' - logger.info(f'players: {player_dict} / data: {data_dict}') + 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"]}' + 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]}' + 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' + 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"])}' + 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 - ) + 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' + 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"]["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' + 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 - ) + 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): +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" - + # 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): + 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) + 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}') - + 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) + 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}') + 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) + await db_delete("games", object_id=db_game_id) except DatabaseError as e: - logger.warning(f'Could not delete game {db_game_id}: {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 + 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 + 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']) + 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') - + 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) + 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') - + 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}) + 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') - + 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 @@ -3528,23 +4280,23 @@ async def complete_game(session: Session, interaction: discord.Interaction, this session, winning_team=winning_team, losing_team=losing_team, - this_game=this_game + this_game=this_game, ) - if 'gauntlet' in this_game.game_type: - logger.info(f'Posting gauntlet results') + 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]), + 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] + 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') - + 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() @@ -3553,30 +4305,24 @@ async def complete_game(session: Session, interaction: discord.Interaction, this session, interaction, this_play, - db_game['id'], + db_game["id"], winning_team=winning_team, losing_team=losing_team, num_potg=3, - num_poop=1 + 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=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 + 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') + news_ticker = get_channel(interaction, "pd-network-news") if news_ticker is not None: await news_ticker.send(content=None, embed=summary_embed) @@ -3588,266 +4334,296 @@ async def complete_game(session: Session, interaction: discord.Interaction, this session.add(this_game) session.commit() - logger.info(f'Just ended game {game_id}') + 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: +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 + 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.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"}' + 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!' +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') + 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') + 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.') + 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', + "Are you sure you want to cancel it?", + label_type="yes", timeout=30, - delete_question=False + delete_question=False, ) if cancel_early: - logger.info(f'{interaction.user.name} is cancelling the game') + 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') + 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.') - + 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.') + 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', + "Should I end this game?", + label_type="yes", timeout=30, - delete_question=False + delete_question=False, ) if forfeit_game: - logger.info(f'{interaction.user.name} is forfeiting the 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 + 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) + db_game = await db_post("games", payload=game_data) except Exception as e: - logger.error(f'Unable to post forfeited game') - + logger.error(f"Unable to post forfeited game") + await interaction.channel.send(content=GAME_DONE_STRING) - news_ticker = get_channel(interaction, 'pd-network-news') + 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.') - + 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') + 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') + 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']): +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') + + 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' + 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') + + 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': + 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') + 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' + 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') + 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') + logger.info(f"corners in, gb 7") this_play = gb_result_7(session, this_play) else: - logger.info(f'Checking if hit to MIF') + 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' + 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', + interaction, + question=f"Were they playing in?", + label_type="yes", ) if playing_in: - logger.info(f'To MIF, batter out, runners hold') + 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') + 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') + 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') + 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}') + 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') + logger.info(f"playing in, gb 7") this_play = gb_result_7(session, this_play) - + else: - logger.info(f'playing in, gb 10') + 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') + 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' + 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') + logger.info(f"Corners in, gb 7") this_play = gb_result_7(session, this_play) - + else: - logger.info(f'Corners in, gb 10') + logger.info(f"Corners in, gb 10") this_play = gb_result_10(session, this_play) else: - logger.info(f'Corners back, gb 2') + logger.info(f"Corners back, gb 2") this_play = gb_result_2(session, this_play) - + else: - logger.info(f'playing back, gb 2') + 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' + 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') + 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') + logger.info(f"playing in, gb 10") this_play = gb_result_10(session, this_play) - + else: - logger.info(f'playing back, gb 2') + 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') + 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}') + 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' + 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: @@ -3855,21 +4631,19 @@ async def groundballs(session: Session, interaction: discord.Interaction, this_p else: playing_in = await ask_confirm( - interaction, - question='Was the defender playing in?', - label_type='yes' + 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') + 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') + logger.info(f"playing in, gb 9") this_play = gb_result_9(session, this_play) - + else: - logger.info(f'playing back, gb 4') + logger.info(f"playing back, gb 4") this_play = gb_result_4(session, this_play) else: @@ -3880,8 +4654,8 @@ async def groundballs(session: Session, interaction: discord.Interaction, this_p else: to_mif = await ask_confirm( interaction, - question='Was that ball hit to 2B/SS?', - label_type='yes' + question="Was that ball hit to 2B/SS?", + label_type="yes", ) if not to_mif and def_align.corners_in: @@ -3891,66 +4665,97 @@ async def groundballs(session: Session, interaction: discord.Interaction, this_p else: playing_in = False - this_play = await gb_letter(session, interaction, this_play, groundball_letter.upper(), 'None', playing_in) - + 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): +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': + + 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]: + + 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]: + + 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']) - + 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']) - + 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: + 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: + + 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': + + elif groundball_letter == "A": return await gb_result(session, interaction, this_play, 7) - - elif groundball_letter == 'B': + + 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): +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}') + 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: @@ -3986,7 +4791,7 @@ async def gb_result(session: Session, interaction: discord.Interaction, this_pla def gb_result_1(session: Session, this_play: Play): - logger.info(f'GB 1') + logger.info(f"GB 1") this_play = advance_runners(session, this_play, 0) this_play.ab, this_play.outs = 1, 1 @@ -3994,7 +4799,7 @@ def gb_result_1(session: Session, this_play: Play): def gb_result_2(session: Session, this_play: Play): - logger.info(f'GB 2') + 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 @@ -4006,21 +4811,21 @@ def gb_result_2(session: Session, this_play: Play): 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') + 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') + logger.info(f"GB 4") if this_play.starting_outs < 2: this_play = advance_runners(session, this_play, 1) @@ -4032,43 +4837,43 @@ def gb_result_4(session: Session, this_play: Play): def gb_result_5(session: Session, this_play: Play, to_mif: bool): - logger.info(f'GB 5') + 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') + 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') + 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') + 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') + 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 @@ -4076,7 +4881,7 @@ def gb_result_9(session: Session, this_play: Play): def gb_result_10(session: Session, this_play: Play): - logger.info(f'GB 10') + 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 @@ -4085,7 +4890,7 @@ def gb_result_10(session: Session, this_play: Play): def gb_result_11(session: Session, this_play: Play): - logger.info(f'GB 11') + 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 @@ -4093,68 +4898,78 @@ def gb_result_11(session: Session, this_play: Play): 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) +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}') - + 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.') - + 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 + 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 + 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 "."}' + 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' + 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: + + 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' + f"Was {runner.card.player.name} thrown out?", + custom_confirm_label="Thrown Out", + custom_cancel_label="Safe", ) if is_lead_out: @@ -4162,37 +4977,45 @@ async def gb_decide(session: Session, this_play: Play, interaction: discord.Inte 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 + 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 - + 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']: +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': + 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']: + 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 @@ -4200,70 +5023,97 @@ 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.' + 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: + 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?' + 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}", ) - 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"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}') + logger.info(f"scores - ai: {ai_score} / human: {human_score}") if abs(ai_score - human_score) >= 7: - need = 'length' + need = "length" elif this_play.inning_num >= 9 and abs(ai_score - human_score) <= 3: - need = 'closer' + need = "closer" elif this_play.inning_num in [7, 8] and abs(ai_score - human_score) <= 3: - need = 'setup' + need = "setup" elif abs(ai_score - human_score) <= 3: - need = 'middle' + need = "middle" else: - need = 'length' - logger.info(f'need: {need}') + 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}' + 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}' + id_string += f"&used_pitcher_ids={x.player_id}" - logger.info(f'used ids: {used_player_ids}') + 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 = '' + cardset_string = "" for x in all_links: - cardset_string += f'&cardset_id={x.cardset_id}' + cardset_string += f"&cardset_id={x.cardset_id}" else: - cardset_string = '' - logger.info(f'cardset_string: {cardset_string}') + 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)) + 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}') - + 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}') + 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}') +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, @@ -4272,16 +5122,16 @@ def substitute_player(session, this_play: Play, old_player: Lineup, new_player: batting_order=old_player.batting_order, game=this_play.game, after_play=max(this_play.play_num - 1, 0), - replacing_id=old_player.id + replacing_id=old_player.id, ) - logger.info(f'new_lineup: {new_lineup}') + logger.info(f"new_lineup: {new_lineup}") session.add(new_lineup) - logger.info(f'De-activating last player') + logger.info(f"De-activating last player") old_player.active = False session.add(old_player) - logger.info(f'Updating play\'s pitcher') + logger.info(f"Updating play's pitcher") this_play.pitcher = new_lineup session.add(this_play) diff --git a/exceptions.py b/exceptions.py index 8bc7b7a..2f0c83f 100644 --- a/exceptions.py +++ b/exceptions.py @@ -129,3 +129,14 @@ class LegalityCheckNotRequired(GameException): class InvalidResponder(GameException): pass + + +class PlayLockedException(GameException): + """ + Raised when attempting to process a play that is already locked by another interaction. + + This prevents concurrent modification of the same play record, which could cause + database deadlocks or data corruption. + """ + + pass diff --git a/health_server.py b/health_server.py index e8ad95c..b4ccbf2 100644 --- a/health_server.py +++ b/health_server.py @@ -3,6 +3,7 @@ HTTP health check server for Paper Dynasty Discord bot. Provides health and readiness endpoints for container monitoring and orchestration. """ + import asyncio import logging from typing import Optional @@ -10,13 +11,13 @@ from aiohttp import web import discord from discord.ext import commands -logger = logging.getLogger('discord_app.health') +logger = logging.getLogger("discord_app.health") class HealthServer: """HTTP server for health checks and metrics.""" - def __init__(self, bot: commands.Bot, host: str = '0.0.0.0', port: int = 8080): + def __init__(self, bot: commands.Bot, host: str = "0.0.0.0", port: int = 8080): """ Initialize health server. @@ -33,9 +34,10 @@ class HealthServer: self.site: Optional[web.TCPSite] = None # Setup routes - self.app.router.add_get('/health', self.health_check) - self.app.router.add_get('/ready', self.readiness_check) - self.app.router.add_get('/metrics', self.metrics) + self.app.router.add_get("/health", self.health_check) + self.app.router.add_get("/ready", self.readiness_check) + self.app.router.add_get("/metrics", self.metrics) + self.app.router.add_get("/diagnostics", self.diagnostics) async def health_check(self, request: web.Request) -> web.Response: """ @@ -43,10 +45,9 @@ class HealthServer: Returns 200 if the server is responsive. """ - return web.json_response({ - 'status': 'healthy', - 'service': 'paper-dynasty-discord-bot' - }) + return web.json_response( + {"status": "healthy", "service": "paper-dynasty-discord-bot"} + ) async def readiness_check(self, request: web.Request) -> web.Response: """ @@ -57,16 +58,19 @@ class HealthServer: 503 if bot is not ready """ if self.bot.is_ready(): - return web.json_response({ - 'status': 'ready', - 'discord_connected': True, - 'latency_ms': round(self.bot.latency * 1000, 2) if self.bot.latency else None - }) + return web.json_response( + { + "status": "ready", + "discord_connected": True, + "latency_ms": round(self.bot.latency * 1000, 2) + if self.bot.latency + else None, + } + ) else: - return web.json_response({ - 'status': 'not_ready', - 'discord_connected': False - }, status=503) + return web.json_response( + {"status": "not_ready", "discord_connected": False}, status=503 + ) async def metrics(self, request: web.Request) -> web.Response: """ @@ -75,33 +79,65 @@ class HealthServer: Provides detailed information about bot state for external monitoring systems. """ metrics_data = { - 'bot': { - 'is_ready': self.bot.is_ready(), - 'is_closed': self.bot.is_closed(), - 'latency_ms': round(self.bot.latency * 1000, 2) if self.bot.latency else None, + "bot": { + "is_ready": self.bot.is_ready(), + "is_closed": self.bot.is_closed(), + "latency_ms": round(self.bot.latency * 1000, 2) + if self.bot.latency + else None, }, - 'guilds': { - 'count': len(self.bot.guilds), - 'guild_ids': [g.id for g in self.bot.guilds] + "guilds": { + "count": len(self.bot.guilds), + "guild_ids": [g.id for g in self.bot.guilds], }, - 'users': { - 'count': len(self.bot.users) - }, - 'cogs': { - 'loaded': list(self.bot.cogs.keys()), - 'count': len(self.bot.cogs) - } + "users": {"count": len(self.bot.users)}, + "cogs": {"loaded": list(self.bot.cogs.keys()), "count": len(self.bot.cogs)}, } return web.json_response(metrics_data) + async def diagnostics(self, request: web.Request) -> web.Response: + """ + Detailed diagnostics for troubleshooting frozen bot. + Captures state before container restart. + """ + import sys + + tasks_info = [] + try: + for task in asyncio.all_tasks(): + tasks_info.append( + { + "name": task.get_name(), + "done": task.done(), + "cancelled": task.cancelled(), + } + ) + except Exception as e: + tasks_info = [f"Error capturing tasks: {e}"] + + diagnostics_data = { + "bot": { + "is_ready": self.bot.is_ready(), + "is_closed": self.bot.is_closed(), + "latency_ms": round(self.bot.latency * 1000, 2) + if self.bot.latency + else None, + }, + "tasks": {"count": len(tasks_info), "tasks": tasks_info[:20]}, + "cogs": {"loaded": list(self.bot.cogs.keys()), "count": len(self.bot.cogs)}, + "python_version": sys.version, + } + + return web.json_response(diagnostics_data) + async def start(self): """Start the health check server.""" self.runner = web.AppRunner(self.app) await self.runner.setup() self.site = web.TCPSite(self.runner, self.host, self.port) await self.site.start() - logger.info(f'Health check server started on {self.host}:{self.port}') + logger.info(f"Health check server started on {self.host}:{self.port}") async def stop(self): """Stop the health check server.""" @@ -109,10 +145,10 @@ class HealthServer: await self.site.stop() if self.runner: await self.runner.cleanup() - logger.info('Health check server stopped') + logger.info("Health check server stopped") -async def run_health_server(bot: commands.Bot, host: str = '0.0.0.0', port: int = 8080): +async def run_health_server(bot: commands.Bot, host: str = "0.0.0.0", port: int = 8080): """ Run health server as a background task.