import asyncio import logging import discord import pandas as pd from sqlmodel import Session, select, func from sqlalchemy import delete from typing import Literal from api_calls import db_delete, db_get, db_post from exceptions import * from helpers import DEFENSE_LITERAL, get_channel from in_game.game_helpers import legal_check from in_game.gameplay_models import Game, Lineup, Team, Play from in_game.gameplay_queries import get_card_or_none, get_channel_game_or_none, get_db_ready_decisions, get_db_ready_plays, get_last_team_play, get_one_lineup, get_player_id_from_dict, get_player_name_from_dict, get_player_or_none, get_sorted_lineups, get_team_or_none, get_players_last_pa, post_game_rewards from utilities.buttons import ButtonOptions, Confirm, ask_confirm from utilities.dropdown import DropdownOptions, DropdownView, 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' } AT_BASE = { 2: 'at second', 3: 'at third', 4: 'at home' } def get_obc(on_first = None, on_second = None, on_third = None) -> int: if on_third is not None: if on_second is not None: if on_first is not None: obc = 7 else: obc = 6 elif on_first is not None: obc = 5 else: obc = 3 elif on_second is not None: if on_first is not None: obc = 4 else: obc = 2 elif on_first is not None: obc = 1 else: obc = 0 return obc def get_re24(this_play: Play, runs_scored: int, new_obc: int, new_starting_outs: int) -> float: re_data = { 0: [0.457, 0.231, 0.077], 1: [0.793, 0.438, 0.171], 2: [1.064, 0.596, 0.259], 4: [1.373, 0.772, 0.351], 3: [1.340, 0.874, 0.287], 5: [1.687, 1.042, 0.406], 6: [1.973, 1.311, 0.448], 7: [2.295, 1.440, 0.618] } start_re24 = re_data[this_play.on_base_code][this_play.starting_outs] end_re24 = 0 if this_play.starting_outs + this_play.outs > 2 else re_data[new_obc][new_starting_outs] return round(end_re24 - start_re24 + runs_scored, 3) def get_wpa(this_play: Play, next_play: Play): """ Returns wpa relative to batting team of this_play. Negative value if bad play, positive value if good play. """ new_rd = next_play.home_score - next_play.away_score if new_rd > 6: new_rd = 6 elif new_rd < -6: new_rd = -6 old_rd = this_play.home_score - this_play.away_score if old_rd > 6: old_rd = 6 elif old_rd < -6: old_rd = -6 # print(f'get_wpa: new_rd = {new_rd} / old_rd = {old_rd}') if (next_play.inning_num >= 9 and new_rd > 0 and next_play.inning_half == 'bot') or (next_play.inning_num > 9 and new_rd > 0 and next_play.is_new_inning): # print(f'manually setting new_win_ex to 1.0') new_win_ex = 1.0 else: new_win_ex = WPA_DF.loc[f'{next_play.inning_half}_{next_play.inning_num}_{next_play.starting_outs}_out_{next_play.on_base_code}_obc_{new_rd}_home_run_diff'].home_win_ex # print(f'new_win_ex = {new_win_ex}') old_win_ex = WPA_DF.loc[f'{this_play.inning_half}_{this_play.inning_num}_{this_play.starting_outs}_out_{this_play.on_base_code}_obc_{old_rd}_home_run_diff'].home_win_ex # print(f'old_win_ex = {old_win_ex}') wpa = float(round(new_win_ex - old_win_ex, 3)) # print(f'final wpa: {wpa}') if this_play.inning_half == 'top': return wpa * -1.0 return wpa def complete_play(session:Session, this_play: Play): """ Commits this_play and new_play """ nso = this_play.starting_outs + this_play.outs runs_scored = 0 on_first, on_second, on_third = None, None, None is_go_ahead = False if nso >= 3: switch_sides = True obc = 0 nso = 0 nih = 'bot' if this_play.inning_half == 'top' else 'top' away_score = this_play.away_score home_score = this_play.home_score try: opponent_play = get_last_team_play(session, this_play.game, this_play.pitcher.team) nbo = opponent_play.batting_order + 1 except PlayNotFoundException as e: logger.info(f'logic_gameplay - complete_play - No last play found for {this_play.pitcher.team.sname}, setting upcoming batting order to 1') nbo = 1 finally: new_batter_team = this_play.game.away_team if nih == 'top' else this_play.game.home_team new_pitcher_team = this_play.game.away_team if nih == 'bot' else this_play.game.home_team inning = this_play.inning_num if nih == 'bot' else this_play.inning_num + 1 else: switch_sides = False nbo = this_play.batting_order + 1 if this_play.pa == 1 else this_play.batting_order nih = this_play.inning_half new_batter_team = this_play.batter.team new_pitcher_team = this_play.pitcher.team inning = this_play.inning_num for this_runner, runner_dest in [ (this_play.batter, this_play.batter_final), (this_play.on_first, this_play.on_first_final), (this_play.on_second, this_play.on_second_final), (this_play.on_third, this_play.on_third_final) ]: if runner_dest is not None: if runner_dest == 1: 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') on_first = this_runner elif runner_dest == 2: 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') on_second = this_runner elif runner_dest == 3: 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') on_third = this_runner elif runner_dest == 4: runs_scored += 1 if this_play.inning_half == 'top': away_score = this_play.away_score + runs_scored home_score = this_play.home_score 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 if runs_scored > 0 and this_play.home_score <= this_play.away_score and home_score > away_score: this_play.is_go_ahead = True obc = get_obc(on_first, on_second, on_third) 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) new_play = Play( game=this_play.game, play_num=this_play.play_num + 1, batting_order=nbo, inning_half=nih, inning_num=inning, starting_outs=nso, on_base_code=obc, away_score=away_score, home_score=home_score, batter=new_batter, batter_pos=new_batter.position, pitcher=get_one_lineup(session, this_play.game, new_pitcher_team, position='P'), catcher=get_one_lineup(session, this_play.game, new_pitcher_team, position='C'), is_new_inning=switch_sides, is_tied=away_score == home_score, on_first=on_first, on_second=on_second, on_third=on_third, managerai=this_play.managerai, 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 session.add(this_play) session.add(new_play) session.commit() session.refresh(new_play) return new_play async def get_lineups_from_sheets(session: Session, sheets, this_game: Game, this_team: Team, lineup_num: int, roster_num: int) -> list[Lineup]: logger.debug(f'sheets: {sheets}') this_sheet = sheets.open_by_key(this_team.gsheet) logger.debug(f'this_sheet: {this_sheet}') r_sheet = this_sheet.worksheet_by_title('My Rosters') logger.debug(f'r_sheet: {r_sheet}') if lineup_num == 1: row_start = 9 row_end = 17 else: row_start = 18 row_end = 26 if roster_num == 1: l_range = f'H{row_start}:I{row_end}' elif roster_num == 2: l_range = f'J{row_start}:K{row_end}' else: l_range = f'L{row_start}:M{row_end}' logger.debug(f'l_range: {l_range}') raw_cells = r_sheet.range(l_range) logger.debug(f'raw_cells: {raw_cells}') try: lineup_cells = [(row[0].value, int(row[1].value)) for row in raw_cells] logger.debug(f'lineup_cells: {lineup_cells}') except ValueError as e: logger.error(f'Could not pull roster for {this_team.abbrev}: {e}') raise ValueError(f'Uh oh. Looks like your roster might not be saved. I am reading blanks when I try to get the card IDs') all_lineups = [] all_pos = [] card_ids = [] for index, row in enumerate(lineup_cells): if '' in row: break if row[0].upper() not in all_pos: all_pos.append(row[0].upper()) else: raise SyntaxError(f'You have more than one {row[0].upper()} in this lineup. Please update and set the lineup again.') this_card = await get_card_or_none(session, card_id=int(row[1])) if this_card is None: raise LookupError( f'Your {row[0].upper()} has a Card ID of {int(row[1])} and I cannot find that card. Did you sell it by chance? Or maybe you sold a duplicate and the bot sold the one you were using?' ) if this_card.team_id != this_team.id: raise SyntaxError(f'Easy there, champ. Looks like card ID {row[1]} belongs to the {this_card.team.lname}. Try again with only cards you own.') card_id = row[1] card_ids.append(str(card_id)) this_lineup = Lineup( position=row[0].upper(), batting_order=index + 1, game=this_game, team=this_team, player=this_card.player, card=this_card ) all_lineups.append(this_lineup) legal_data = await legal_check([card_ids], difficulty_name=this_game.game_type) logger.debug(f'legal_data: {legal_data}') if not legal_data['legal']: raise CardLegalityException(f'The following cards appear to be illegal for this game mode:\n{legal_data["error_string"]}') if len(all_lineups) != 9: raise Exception(f'I was only able to pull in {len(all_lineups)} batters from Sheets. Please check your saved lineup and try again.') return all_lineups async def checks_log_interaction(session: Session, interaction: discord.Interaction, command_name: str) -> tuple[Game, Team, Play]: """ Commits this_play """ await interaction.response.defer(thinking=True) this_game = get_channel_game_or_none(session, interaction.channel_id) if this_game is None: raise GameNotFoundException('I don\'t see an active game in this channel.') owner_team = await get_team_or_none(session, gm_id=interaction.user.id) if owner_team is None: logger.exception(f'{command_name} command: No team found for GM ID {interaction.user.id}') raise TeamNotFoundException(f'Do I know you? I cannot find your team.') if 'gauntlet' in this_game.game_type: gauntlet_abbrev = f'Gauntlet-{owner_team.abbrev}' owner_team = await get_team_or_none(session, team_abbrev=gauntlet_abbrev) if owner_team is None: logger.exception(f'{command_name} command: No gauntlet team found with abbrev {gauntlet_abbrev}') raise TeamNotFoundException(f'Hm, I was not able to find a gauntlet team for you.') if not owner_team.id in [this_game.away_team_id, this_game.home_team_id]: if interaction.user.id != 258104532423147520: logger.exception(f'{interaction.user.display_name} tried to run a command in Game {this_game.id} when they aren\'t a GM in the game.') raise TeamNotFoundException('Bruh. Only GMs of the active teams can log plays.') else: await interaction.channel.send(f'Cal is bypassing the GM check to run the {command_name} command') this_play = this_game.current_play_or_none(session) if this_play is None: logger.error(f'{command_name} command: No play found for Game ID {this_game.id} - attempting to initialize play') this_play = activate_last_play(session, this_game) this_play.locked = True session.add(this_play) session.commit() return this_game, owner_team, this_play def log_run_scored(session: Session, runner: Lineup, this_play: Play, is_earned: bool = True): """ Commits last_ab """ last_ab = get_players_last_pa(session, lineup_member=runner) last_ab.run = 1 errors = session.exec(select(func.count(Play.id)).where( Play.inning_num == last_ab.inning_num, Play.inning_half == last_ab.inning_half, Play.error == 1 )).one() outs = session.exec(select(func.sum(Play.outs)).where( Play.inning_num == last_ab.inning_num, Play.inning_half == last_ab.inning_half )).one() if errors + outs + this_play.error >= 3: is_earned = False last_ab.e_run = 1 if is_earned else 0 session.add(last_ab) session.commit() return True def advance_runners(session: Session, this_play: Play, num_bases: int, is_error: bool = False, only_forced: bool = False) -> Play: """ No commits """ logger.info(f'Advancing runners {num_bases} bases in game {this_play.game.id}') this_play.rbi = 0 if num_bases == 0: if this_play.on_first is not None: this_play.on_first_final = 1 if this_play.on_second_id is not None: this_play.on_second_final = 2 if this_play.on_third_id is not None: this_play.on_third_final = 3 elif only_forced: if not this_play.on_first: if this_play.on_second: this_play.on_second_final = 2 if this_play.on_third: this_play.on_third_final = 3 if 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) this_play.rbi += 1 if not is_error else 0 if num_bases > 1: this_play.on_second_final = 4 log_run_scored(session, this_play.on_second, this_play) this_play.rbi += 1 if not is_error else 0 elif num_bases == 1: this_play.on_second_final = 3 else: this_play.on_second_final = 2 else: if this_play.on_third: this_play.on_third_final = 3 if num_bases > 2: this_play.on_first_final = 4 log_run_scored(session, this_play.on_first, this_play) this_play.rbi += 1 if not is_error else 0 elif num_bases == 2: this_play.on_first_final = 3 elif num_bases == 1: this_play.on_first_final = 2 else: this_play.on_first_final = 1 else: if this_play.on_third: if num_bases > 0: this_play.on_third_final = 4 log_run_scored(session, this_play.on_third, this_play) this_play.rbi += 1 if not is_error else 0 else: this_play.on_third_final = 3 if this_play.on_second: if num_bases > 1: this_play.on_second_final = 4 log_run_scored(session, this_play.on_second, this_play) this_play.rbi += 1 if not is_error else 0 elif num_bases == 1: this_play.on_second_final = 3 else: this_play.on_second_final = 2 if this_play.on_first: if num_bases > 2: this_play.on_first_final = 4 log_run_scored(session, this_play.on_first, this_play) this_play.rbi += 1 if not is_error else 0 elif num_bases == 2: this_play.on_first_final = 3 elif num_bases == 1: this_play.on_first_final = 2 else: this_play.on_first_final = 1 return this_play async def show_outfield_cards(session: Session, interaction: discord.Interaction, this_play: Play) -> Lineup: lf = get_one_lineup(session, this_game=this_play.game, this_team=this_play.pitcher.team, position='LF') cf = get_one_lineup(session, this_game=this_play.game, this_team=this_play.pitcher.team, position='CF') rf = get_one_lineup(session, this_game=this_play.game, this_team=this_play.pitcher.team, position='RF') this_team = this_play.pitcher.team logger.debug(f'lf: {lf.player.name_with_desc}\n\ncf: {cf.player.name_with_desc}\n\nrf: {rf.player.name_with_desc}\n\nteam: {this_team.lname}') view = Pagination([interaction.user], timeout=10) view.left_button.label = f'Left Fielder' view.left_button.style = discord.ButtonStyle.secondary lf_embed = image_embed( image_url=lf.player.image, title=f'{this_team.sname} LF', color=this_team.color, desc=lf.player.name, author_name=this_team.lname, author_icon=this_team.logo ) view.cancel_button.label = f'Center Fielder' view.cancel_button.style = discord.ButtonStyle.blurple cf_embed = image_embed( image_url=cf.player.image, title=f'{this_team.sname} CF', color=this_team.color, desc=cf.player.name, author_name=this_team.lname, author_icon=this_team.logo ) view.right_button.label = f'Right Fielder' view.right_button.style = discord.ButtonStyle.secondary rf_embed = image_embed( image_url=rf.player.image, title=f'{this_team.sname} RF', color=this_team.color, desc=rf.player.name, author_name=this_team.lname, author_icon=this_team.logo ) page_num = 1 embeds = [lf_embed, cf_embed, rf_embed] msg = await interaction.channel.send(embed=embeds[page_num], view=view) await view.wait() if view.value: if view.value == 'left': page_num = 0 if view.value == 'cancel': page_num = 1 if view.value == 'right': page_num = 2 else: await msg.edit(content=None, embed=embeds[page_num], view=None) view.value = None if page_num == 0: view.left_button.style = discord.ButtonStyle.blurple view.cancel_button.style = discord.ButtonStyle.secondary view.right_button.style = discord.ButtonStyle.secondary if page_num == 1: view.left_button.style = discord.ButtonStyle.secondary view.cancel_button.style = discord.ButtonStyle.blurple view.right_button.style = discord.ButtonStyle.secondary if page_num == 2: view.left_button.style = discord.ButtonStyle.secondary view.cancel_button.style = discord.ButtonStyle.secondary view.right_button.style = discord.ButtonStyle.blurple view.left_button.disabled = True view.cancel_button.disabled = True view.right_button.disabled = True await msg.edit(content=None, embed=embeds[page_num], view=view) return [lf, cf, rf][page_num] async def flyballs(session: Session, interaction: discord.Interaction, this_play: Play, flyball_type: Literal['a', 'ballpark', 'b', 'b?', 'c']) -> Play: """ Commits this_play """ this_game = this_play.game num_outs = 1 if flyball_type == 'a': this_play.pa, this_play.ab, this_play.outs = 1, 1, 1 if this_play.starting_outs < 2: advance_runners(session, this_play, num_bases=1) if this_play.on_third: this_play.ab = 0 elif flyball_type == 'b' or flyball_type == 'ballpark': this_play.pa, this_play.ab, this_play.outs = 1, 1, 1 this_play.bpfo = 1 if flyball_type == 'ballpark' else 0 advance_runners(session, this_play, num_bases=0) if this_play.starting_outs < 2 and this_play.on_third: this_play.ab = 0 this_play.rbi = 1 this_play.on_third_final = 4 log_run_scored(session, this_play.on_third, this_play) if this_play.starting_outs < 2 and this_play.on_second: logger.debug(f'calling of embed') await show_outfield_cards(session, interaction, this_play) logger.debug(f'done with of embed') runner = this_play.on_second.player view = Confirm(responders=[interaction.user], timeout=60, label_type='yes') if this_play.ai_is_batting: tag_resp = this_play.managerai.tag_from_second(session, this_game) q_text = f'{runner.name} will attempt to advance to third if the safe range is **{tag_resp.min_safe}+**, are they going?' else: q_text = f'Is {runner.name} attempting to tag up to third?' question = await interaction.channel.send( content=q_text, view=view ) await view.wait() if view.value: await question.delete() view = ButtonOptions( responders=[interaction.user], timeout=60, labels=['Tagged Up', 'Hold at 2nd', 'Out at 3rd', None, None] ) question = await interaction.channel.send( f'What was the result of {runner.name} tagging from second?', view=view ) await view.wait() if view.value: await question.delete() if view.value == 'Tagged Up': this_play.on_second_final = 3 elif view.value == 'Out at 3rd': num_outs += 1 this_play.on_second_final = None this_play.outs = num_outs else: await question.delete() else: await question.delete() elif flyball_type == 'b?': this_play.pa, this_play.ab, this_play.outs = 1, 1, 1 if this_play.starting_outs < 2 and this_play.on_third: logger.debug(f'calling of embed') await show_outfield_cards(session, interaction, this_play) logger.debug(f'done with of embed') runner = this_play.on_second.player view = Confirm(responders=[interaction.user], timeout=60, label_type='yes') if this_play.ai_is_batting: tag_resp = this_play.managerai.tag_from_second(session, this_game) q_text = f'{runner.name} will attempt to advance home if the safe range is **{tag_resp.min_safe}+**, are they going?' else: q_text = f'Is {runner.name} attempting to tag up and go home?' question = await interaction.channel.send( content=q_text, view=view ) await view.wait() if view.value: await question.delete() view = Confirm(responders=[interaction.user], timeout=60, label_type='yes') question = await interaction.channel.send( f'Was {runner.name} thrown out?', view=view ) await view.wait() if view.value: await question.delete() num_outs += 1 this_play.on_third_final = 99 this_play.outs = num_outs else: await question.delete() this_play.ab = 0 this_play.rbi = 1 this_play.on_third_final = 4 log_run_scored(session, this_play.on_third, this_play) else: await question.delete() elif flyball_type == 'c': this_play.pa, this_play.ab, this_play.outs = 1, 1, 1 advance_runners(session, this_play, num_bases=0) session.add(this_play) session.commit() session.refresh(this_play) return this_play 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}') def_team = this_play.pitcher.team # Either there is no AI team or the AI is pitching if not this_game.ai_team or not this_play.ai_is_batting: is_lead_running = await ask_confirm( interaction=interaction, question=f'Is **{lead_runner.player.name}** being sent {TO_BASE[lead_base]}?', label_type='yes' ) if is_lead_running: throw_resp = None if this_game.ai_team: throw_resp = this_play.managerai.throw_at_uncapped(session, this_game) logger.info(f'throw_resp: {throw_resp}') if throw_resp.cutoff: await interaction.channel.send(f'The {def_team.sname} will cut off the throw {TO_BASE[lead_base]}') if this_play.on_second == lead_runner: this_play.rbi += 1 this_play.on_second_final = 4 log_run_scored(session, lead_runner, this_play) else: this_play.on_first_final = 3 await asyncio.sleep(1) return this_play else: throw_for_lead = await ask_confirm( interaction=interaction, question=f'Is the defense throwing {TO_BASE[lead_base]} for {lead_runner.player.name}?', label_type='yes' ) # Human defense is cutting off the throw if not throw_for_lead: await question.delete() if this_play.on_second == lead_runner: this_play.rbi += 1 this_play.on_second_final = 4 log_run_scored(session, lead_runner, this_play) return this_play # Human runner is advancing, defense is throwing trail_advancing = await ask_confirm( interaction=interaction, question=f'Is **{trail_runner.player.name}** being sent {TO_BASE[trail_base]} as the trail runner?', label_type='yes' ) # Trail runner is advancing if trail_advancing: 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' ai_throw_lead = False if this_game.ai_team: if throw_resp.at_trail_runner: question = await interaction.channel.send( f'The {def_team.sname} will throw for the trail runner if both:\n- {trail_runner.player.name}\'s safe range is {throw_resp.trail_max_safe} or lower\n- {trail_runner.player.name}\'s safe range is lower than {lead_runner.player.name}\'s by at least {abs(throw_resp.trail_max_safe_delta)}.\n\nIs the throw going {TO_BASE[lead_base]} or {TO_BASE[trail_base]}?', view=view ) else: await interaction.channel.send(f'**{outfielder.player.name}** will throw {TO_BASE[lead_base]}!') ai_throw_lead = True else: question = await interaction.channel.send( f'Is the throw going {TO_BASE[lead_base]} or {TO_BASE[trail_base]}?', view=view ) if not ai_throw_lead: await view.wait() elif ai_throw_lead: view.value = True # Throw is going to lead runner if view.value: try: await question.delete() except (discord.NotFound, UnboundLocalError): pass if this_play.on_first == trail_runner: this_play.on_first_final += 1 elif this_play.batter == trail_runner: this_play.batter_final += 1 else: log_exception(LineupsMissingException, f'Could not find trail runner to advance') # Throw is going to trail runner else: try: await question.delete() except (discord.NotFound, UnboundLocalError): pass runner_thrown_out = await ask_confirm( interaction=interaction, question='Was **{trail_runner.player.name}** thrown out {AT_BASE[trail_base]}?', label_type='yes' ) # Trail runner is thrown out if runner_thrown_out: # Log out on play this_play.outs += 1 # Remove trail runner if this_play.on_first == trail_runner: this_play.on_first_final = None else: this_play.batter_final = None # 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) return this_play # Ball is going to lead base, ask if safe runner_thrown_out = await ask_confirm( interaction=interaction, question=f'Was **{lead_runner.player.name}** thrown out {AT_BASE[lead_base]}?', label_type='yes' ) # Lead runner is thrown out if runner_thrown_out: logger.info(f'Lead runner is thrown out.') this_play.outs += 1 # Lead runner is safe else: logger.info(f'Lead runner is safe.') if this_play.on_second == lead_runner: logger.info(f'setting lead runner on_second_final') this_play.on_second_final = None if runner_thrown_out else lead_base elif this_play.on_first == lead_runner: logger.info(f'setting lead runner on_first') this_play.on_first_final = None if runner_thrown_out else lead_base else: log_exception(LineupsMissingException, f'Could not find lead runner to set final destination') # Human lead runner is not advancing else: return this_play elif this_play.ai_is_batting: run_resp = this_play.managerai.uncapped_advance(session, this_game, lead_base, trail_base) is_lead_running = await ask_confirm( interaction=interaction, question=f'**{lead_runner.player.name}** will advance {TO_BASE[lead_base]} if the safe range is {run_resp.min_safe} or higher.\n\nIs **{lead_runner.player.name}** attempting to advance?', label_type='yes' ) if not is_lead_running: return this_play is_defense_throwing = await ask_confirm( interaction=interaction, question=f'Is the defense throwing {TO_BASE[lead_base]} for {lead_runner.player.name}?', label_type='yes' ) # Human defense is throwing for lead runner if not is_defense_throwing: 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 = 3 return this_play # Human throw is not being cut off if run_resp.send_trail: await interaction.channel.send( f'**{trail_runner.player.name}** is advancing {TO_BASE[trail_base]} as the trail runner!', ) is_throwing_lead = await ask_confirm( interaction=interaction, question=f'Is the throw going {TO_BASE[lead_base]} or {TO_BASE[trail_base]}?', label_type='yes', custom_confirm_label='Home Plate' if lead_base == 4 else 'Third Base', custom_cancel_label='Third Base' if trail_base == 3 else 'Second Base' ) # Trail runner advances, throwing for lead runner if is_throwing_lead: if this_play.on_first == trail_runner: this_play.on_first_final += 1 elif this_play.batter == trail_runner: this_play.batter_final += 1 else: log_exception(LineupsMissingException, f'Could not find trail runner to advance') # Throw is going to trail runner else: is_trail_out = await ask_confirm( interaction=interaction, question=f'Was **{trail_runner.player.name}** thrown out {AT_BASE[trail_base]}?', label_type='yes' ) if is_trail_out: # Log out on play this_play.outs += 1 # Remove trail runner if this_play.on_first == trail_runner: this_play.on_first_final = None else: this_play.batter_final = None # 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) return this_play # Ball is going to lead base, ask if safe is_lead_out = await ask_confirm( interaction=interaction, question=f'Was **{lead_runner.player.name}** thrown out {AT_BASE[lead_base]}?', label_type='yes', ) # Lead runner is thrown out if is_lead_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') this_play.on_second_final = None if is_lead_out else lead_base elif this_play.on_first == lead_runner: logger.info(f'setting lead runner on_first') this_play.on_first_final = None if is_lead_out else lead_base else: log_exception(LineupsMissingException, f'Could not find lead runner to set final destination') return this_play async def singles(session: Session, interaction: discord.Interaction, this_play: Play, single_type: Literal['*', '**', 'ballpark', 'uncapped']) -> Play: """ Commits this_play """ this_play.hit, this_play.batter_final = 1, 1 if single_type == '**': advance_runners(session, this_play, num_bases=2) elif single_type in ['*', 'ballpark']: advance_runners(session, this_play, num_bases=1) this_play.bp1b = 1 if single_type == 'ballpark' else 0 elif single_type == 'uncapped': advance_runners(session, this_play, 1) if this_play.on_base_code in [1, 2, 4, 5, 6, 7]: if this_play.on_second: lead_runner = this_play.on_second lead_base = 4 if this_play.on_first: trail_runner = this_play.on_first trail_base = 3 else: trail_runner = this_play.batter trail_base = 2 else: lead_runner = this_play.on_first lead_base = 3 trail_runner = this_play.batter trail_base = 2 this_play = await check_uncapped_advance(session, interaction, this_play, lead_runner, lead_base, trail_runner, trail_base) session.add(this_play) session.commit() session.refresh(this_play) return this_play async def doubles(session: Session, interaction: discord.Interaction, this_play: Play, double_type: Literal['**', '***', 'uncapped']) -> Play: """ Commits this_play """ this_play.hit, this_play.double, this_play.batter_final = 1, 1, 2 if double_type == '**': this_play = advance_runners(session, this_play, num_bases=2) elif double_type == '***': this_play = advance_runners(session, this_play, num_bases=3) elif double_type == 'uncapped': this_play = advance_runners(session, this_play, num_bases=2) if this_play.on_first: this_play = await check_uncapped_advance(session, interaction, this_play, lead_runner=this_play.on_first, lead_base=4, trail_runner=this_play.batter, trail_base=3) session.add(this_play) session.commit() session.refresh(this_play) return this_play async def triples(session: Session, interaction: discord.Interaction, this_play: Play): """ Commits this play """ this_play.hit, this_play.triple, this_play.batter_final = 1, 1, 3 this_play = advance_runners(session, this_play, num_bases=3) session.add(this_play) session.commit() session.refresh(this_play) return this_play async def homeruns(session: Session, interaction: discord.Interaction, this_play: Play, homerun_type: Literal['ballpark', 'no-doubt']): this_play.hit, this_play.homerun, this_play.batter_final, this_play.run = 1, 1, 4, 1 this_play.bphr = 1 if homerun_type == 'ballpark' else 0 this_play = advance_runners(session, this_play, num_bases=4) this_play.rbi += 1 session.add(this_play) session.commit() session.refresh(this_play) return this_play async def walks(session: Session, interaction: discord.Interaction, this_play: Play, walk_type: Literal['unintentional', 'intentional'] = 'unintentional'): this_play.ab, this_play.bb, this_play.batter_final = 0, 1, 1 this_play.ibb = 1 if walk_type == 'intentional' else 0 this_play = advance_runners(session, this_play, num_bases=1, only_forced=True) session.add(this_play) session.commit() session.refresh(this_play) return this_play async def strikeouts(session: Session, interaction: discord.Interaction, this_play: Play): this_play.so, this_play.outs = 1, 1 this_play = advance_runners(session, this_play, num_bases=0) session.add(this_play) session.commit() session.refresh(this_play) return this_play async def popouts(session: Session, interaction: discord.Interaction, this_play: Play): this_play.outs = 1 this_play = advance_runners(session, this_play, num_bases=0) session.add(this_play) session.commit() session.refresh(this_play) return this_play async def hit_by_pitch(session: Session, interaction: discord.Interaction, this_play: Play): this_play.ab, this_play.hbp = 0, 1 this_play = advance_runners(session, this_play, num_bases=1, only_forced=True) session.add(this_play) session.commit() session.refresh(this_play) return this_play async def bunts(session: Session, interaction: discord.Interaction, this_play: Play, bunt_type: Literal['sacrifice', 'bad', 'popout', 'double-play', 'defense']): this_play.ab = 1 if bunt_type != 'sacrifice' else 0 this_play.sac = 1 if bunt_type != 'sacrifice' else 0 this_play.outs = 1 if bunt_type == 'sacrifice': this_play = advance_runners(session, this_play, num_bases=1) elif bunt_type == 'popout': this_play = advance_runners(session, this_play, num_bases=0) elif bunt_type == 'bad': this_play = advance_runners(session, this_play, num_bases=1) this_play.batter_final = 1 if this_play.on_third is not None: this_play.on_third_final = None elif this_play.on_second is not None: this_play.on_second_final = None elif this_play.on_first is not None: this_play.on_first_final = None elif bunt_type == 'double-play': this_play = advance_runners(session, this_play, num_bases=0) this_play.outs = 2 if this_play.starting_outs < 2 else 1 if this_play.on_third is not None: this_play.on_third_final = None elif this_play.on_second is not None: this_play.on_second_final = None elif this_play.on_first is not None: this_play.on_first_final = None else: log_exception(KeyError, f'Bunt type {bunt_type} is not yet implemented') session.add(this_play) session.commit() session.refresh(this_play) return this_play def activate_last_play(session: Session, this_game: Game) -> Play: p_query = session.exec(select(Play).where(Play.game == this_game).order_by(Play.id.desc()).limit(1)).all() this_play = complete_play(session, p_query[0]) return this_play def undo_play(session: Session, this_play: Play): this_game = this_play.game last_two_plays = session.exec(select(Play).where(Play.game == this_game).order_by(Play.id.desc()).limit(2)).all() for play in last_two_plays: for runner, to_base in [(play.on_first, play.on_first_final), (play.on_second, play.on_second_final), (play.on_third, play.on_third_final)]: if to_base == 4: last_pa = get_players_last_pa(session, runner) last_pa.run, last_pa.e_run = 0, 0 session.add(last_pa) last_two_ids = [last_two_plays[0].id, last_two_plays[1].id] logger.warning(f'Deleting plays: {last_two_ids}') session.exec(delete(Play).where(Play.id.in_(last_two_ids))) session.commit() try: this_play = this_game.initialize_play(session) logger.info(f'Initialized play: {this_play.id}') except PlayInitException: this_play = activate_last_play(session, this_game) logger.info(f'Re-activated play: {this_play.id}') return this_play async def show_defense_cards(session: Session, interaction: discord.Interaction, this_play: Play, first_position: DEFENSE_LITERAL): position_map = { 'Pitcher': 'P', 'Catcher': 'C', 'First Base': '1B', 'Second Base': '2B', 'Third Base': '3B', 'Shortstop': 'SS', 'Left Field': 'LF', 'Center Field': 'CF', 'Right Field': 'RF' } this_position = position_map[first_position] sorted_lineups = get_sorted_lineups(session, this_play.game, this_play.pitcher.team) select_player_options = [ discord.SelectOption(label=f'{x.position} - {x.player.name}', value=f'{x.id}', default=this_position == x.position) for x in sorted_lineups ] this_lineup = get_one_lineup(session, this_play.game, this_play.pitcher.team, position=this_position) player_embed = image_embed( image_url=this_lineup.player.image, color=this_play.pitcher.team.color, author_name=this_play.pitcher.team.lname, author_icon=this_play.pitcher.team.logo ) player_dropdown = SelectViewDefense( options=select_player_options, this_play=this_play, base_embed=player_embed, session=session, sorted_lineups=sorted_lineups ) dropdown_view = DropdownView(dropdown_objects=[player_dropdown], timeout=60) await interaction.edit_original_response(content=None, embed=player_embed, view=dropdown_view) def is_game_over(this_play: Play) -> bool: print(f'1: ') if this_play.inning_num < 9 and (abs(this_play.away_score - this_play.home_score) < 10): return False if abs(this_play.away_score - this_play.home_score) >= 10: if ((this_play.home_score - this_play.away_score) >= 10) and this_play.inning_half == 'bot': return True elif ((this_play.away_score - this_play.home_score) >= 10) and this_play.is_new_inning and this_play.inning_half == 'top': return True if this_play.inning_num > 9 and this_play.inning_half == 'top' and this_play.is_new_inning and this_play.home_score != this_play.away_score: return True if this_play.inning_num >= 9 and this_play.inning_half == 'bot' and this_play.home_score > this_play.away_score: return True return False async def get_game_summary_embed(session: Session, interaction: discord.Interaction, this_play: Play, db_game_id: int, winning_team: Team, losing_team: Team, num_potg: int = 1, num_poop: int = 0): game_summary = await db_get(f'plays/game-summary/{db_game_id}', params=[('tp_max', num_potg)]) this_game = this_play.game game_embed = winning_team.embed game_embed.title = f'{this_game.away_team.lname} {this_play.away_score} @ {this_play.home_score} {this_game.home_team.lname} - F/{this_play.inning_num}' game_embed.add_field( name='Location', value=f'{interaction.guild.get_channel(this_game.channel_id).mention}' ) game_embed.add_field(name='Game ID', value=f'{db_game_id}') if this_game.game_type == 'major-league': game_des = 'Major League' elif this_game.game_type == 'minor-league': game_des = 'Minor League' elif this_game.game_type == 'hall-of-fame': game_des = 'Hall of Fame' elif this_game.game_type == 'flashback': game_des = 'Flashback' elif this_game.ranked: game_des = 'Ranked' elif 'gauntlet' in this_game.game_type: game_des = 'Gauntlet' else: game_des = 'Unlimited' game_embed.description = f'Score Report - {game_des}' game_embed.add_field( name='Box Score', value=f'```\n' f'Team | R | H | E |\n' f'{this_game.away_team.abbrev.replace("Gauntlet-", ""): <4} | {game_summary["runs"]["away"]: >2} | ' f'{game_summary["hits"]["away"]: >2} | {game_summary["errors"]["away"]: >2} |\n' f'{this_game.home_team.abbrev.replace("Gauntlet-", ""): <4} | {game_summary["runs"]["home"]: >2} | ' f'{game_summary["hits"]["home"]: >2} | {game_summary["errors"]["home"]: >2} |\n' f'\n```', inline=False ) logger.info(f'getting top players string') potg_string = '' for tp in game_summary['top-players']: player_name = f'{get_player_name_from_dict(tp['player'])}' potg_line = f'{player_name} - ' if 'hr' in tp: potg_line += f'{tp["hit"]}-{tp["ab"]}' if tp['hr'] > 0: num = f'{tp["hr"]} ' if tp["hr"] > 1 else "" potg_line += f', {num}HR' if tp['triple'] > 0: num = f'{tp["triple"]} ' if tp["triple"] > 1 else "" potg_line += f', {num}3B' if tp['double'] > 0: num = f'{tp["double"]} ' if tp["double"] > 1 else "" potg_line += f', {num}2B' if tp['run'] > 0: potg_line += f', {tp["run"]} R' if tp['rbi'] > 0: potg_line += f', {tp["rbi"]} RBI' else: potg_line = f'{player_name} - {tp["ip"]} IP, {tp["run"]} R' if tp['run'] != tp['e_run']: potg_line += f' ({tp["e_run"]} ER)' potg_line += f', {tp["hit"]} H, {tp["so"]} K' potg_line += f', {tp["re24"]:.2f} re24\n' potg_string += potg_line game_embed.add_field( name='Players of the Game', value=potg_string, inline=False ) logger.info(f'getting pooper string') poop_string = '' if 'pooper' in game_summary and game_summary['pooper'] is not None: if isinstance(game_summary['pooper'], dict): all_poop = [game_summary['pooper']] elif isinstance(game_summary['pooper'], list): all_poop = game_summary['pooper'] for line in all_poop: poop_line = f'{player_name} - ' player_name = f'{get_player_name_from_dict(tp['player'])}' 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', {tp["re24"]:.2f} re24\n' poop_string += poop_line if len(poop_string) > 0: game_embed.add_field( 'Pooper of the Game', value=poop_string, inline=False ) pit_string = f'Win: {game_summary["pitchers"]["win"]["p_name"]}\nLoss: {game_summary["pitchers"]["loss"]["p_name"]}\n' hold_string = None for player in game_summary['pitchers']['holds']: player_name = f'{get_player_name_from_dict(player)}' if hold_string is None: hold_string = f'Holds: {player_name}' else: hold_string += f', {player_name}' if hold_string is not None: pit_string += f'{hold_string}\n' if game_summary['pitchers']['save'] is not None: player_name = f'{get_player_name_from_dict(game_summary["pitchers"]["save"])}' pit_string += f'Save: {player_name}' game_embed.add_field( name=f'Pitching', value=pit_string, ) def name_list(raw_list: list) -> str: logger.info(f'raw_list: {raw_list}') player_dict = {} for x in raw_list: if x['player_id'] not in player_dict: player_dict[x['player_id']] = x data_dict = {} for x in raw_list: if x['player_id'] not in data_dict: data_dict[x['player_id']] = 1 else: data_dict[x['player_id']] += 1 r_string = '' logger.info(f'players: {player_dict} / data: {data_dict}') first = True for p_id in data_dict: r_string += f'{", " if not first else ""}{player_dict[p_id]["p_name"]}' if data_dict[p_id] > 1: r_string += f' {data_dict[p_id]}' first = False return r_string logger.info(f'getting running string') if len(game_summary['running']['sb']) + len(game_summary['running']['csc']) > 0: run_string = '' if len(game_summary['running']['sb']) > 0: run_string += f'SB: {name_list(game_summary["running"]["sb"])}\n' if len(game_summary['running']['csc']) > 0: run_string += f'CSc: {name_list(game_summary["running"]["csc"])}' game_embed.add_field( name=f'Baserunning', value=run_string ) logger.info(f'getting xbh string') if len(game_summary['xbh']['2b']) + len(game_summary['xbh']['3b']) + len(game_summary['xbh']['hr']) > 0: bat_string = '' if len(game_summary['xbh']['2b']) > 0: bat_string += f'2B: {name_list(game_summary["xbh"]["2b"])}\n' if len(game_summary['xbh']['3b']) > 0: bat_string += f'3B: {name_list(game_summary["xbh"]["3b"])}\n' if len(game_summary['xbh']['hr']) > 0: bat_string += f'HR: {name_list(game_summary["xbh"]["hr"])}\n' else: bat_string = 'Oops! All bitches! No XBH from either team.' game_embed.add_field( name='Batting', value=bat_string, inline=False ) return game_embed async def complete_game(session: Session, interaction: discord.Interaction, this_play: Play): # if interaction is not None: # salutation = await interaction.channel.send('GGs, I\'ll tally this game up...') # Add button with {winning_team} wins! and another with "Roll Back" this_game = this_play.game async def roll_back(db_game_id: int, game: bool = True, plays: bool = False, decisions: bool = False): if decisions: try: await db_delete('decisions/game', object_id=db_game_id) except DatabaseError as e: logger.warning(f'Could not delete decisions for game {db_game_id}: {e}') if plays: try: await db_delete('plays/game', object_id=db_game_id) except DatabaseError as e: logger.warning(f'Could not delete plays for game {db_game_id}: {e}') if game: try: await db_delete('games', object_id=db_game_id) except DatabaseError as e: logger.warning(f'Could not delete game {db_game_id}: {e}') # Post completed game to API game_data = this_game.model_dump() game_data['home_team_ranking'] = this_game.home_team.ranking game_data['away_team_ranking'] = this_game.away_team.ranking game_data['home_team_value'] = this_game.home_team.team_value game_data['away_team_value'] = this_game.away_team.team_value game_data['away_score'] = this_play.away_score game_data['home_score'] = this_play.home_score winning_team = this_game.home_team if this_play.home_score > this_play.away_score else this_game.away_team losing_team = this_game.home_team if this_play.away_score > this_play.home_score else this_game.away_team try: db_game = await db_post('games', payload=game_data) db_ready_plays = get_db_ready_plays(session, this_game, db_game['id']) db_ready_decisions = get_db_ready_decisions(session, this_game, db_game['id']) except Exception as e: await roll_back(db_game['id']) log_exception(e, msg='Unable to post game to API, rolling back') # Post game stats to API try: resp = await db_post('plays', payload=db_ready_plays) except Exception as e: await roll_back(db_game['id'], plays=True) log_exception(e, msg='Unable to post plays to API, rolling back') if len(resp) > 0: pass try: resp = await db_post('decisions', payload={'decisions': db_ready_decisions}) except Exception as e: await roll_back(db_game['id'], plays=True, decisions=True) log_exception(e, msg='Unable to post decisions to API, rolling back') if len(resp) > 0: pass # Post game rewards (gauntlet and main team) try: win_reward, loss_reward = await post_game_rewards( session, winning_team=winning_team, losing_team=losing_team, this_game=this_game ) except Exception as e: await roll_back(db_game['id'], plays=True, decisions=True) log_exception(e, msg='Error while posting game rewards') session.delete(this_play) session.commit() # Pull game summary for embed summary_embed = await get_game_summary_embed( session, interaction, this_play, db_game['id'], winning_team=winning_team, losing_team=losing_team, num_potg=3, num_poop=1 ) summary_embed.add_field( name=f'{winning_team.abbrev} Rewards', value=win_reward ) summary_embed.add_field( name=f'{losing_team.abbrev} Rewards', value=loss_reward ) summary_embed.add_field( name='Highlights', value=f'Please share the highlights in {get_channel(interaction, "pd-news-ticker").mention}!', inline=False ) # Create and post game summary to game channel and pd-network-news news_ticker = get_channel(interaction, 'pd-network-news') if news_ticker is not None: await news_ticker.send(content=None, embed=summary_embed) await interaction.channel.send(content=None, embed=summary_embed) await interaction.edit_original_response(content=None, embed=summary_embed) game_id = this_game.id this_game.active = False session.add(this_game) session.commit() logger.info(f'Just ended game {game_id}')