From 8118b4a69150b73837eba80e97f7324a72c3aed2 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Fri, 6 Dec 2024 00:46:49 -0600 Subject: [PATCH] Added PO/FO to fielding roll Complete /log xcheck and /log groundball --- cogs/gameplay.py | 18 +- cogs/owner.py | 2 +- command_logic/logic_gameplay.py | 495 ++++++++++++++++-- dice.py | 24 +- in_game/gameplay_models.py | 85 +-- in_game/gameplay_queries.py | 152 ++++-- in_game/managerai_responses.py | 20 +- tests/command_logic/test_logic_gameplay.py | 16 +- tests/command_logic/test_logic_groundballs.py | 348 ++++++++++++ utilities/buttons.py | 2 + 10 files changed, 1016 insertions(+), 146 deletions(-) create mode 100644 tests/command_logic/test_logic_groundballs.py diff --git a/cogs/gameplay.py b/cogs/gameplay.py index ecda284..2cee043 100644 --- a/cogs/gameplay.py +++ b/cogs/gameplay.py @@ -11,7 +11,7 @@ import pygsheets from sqlmodel import or_ from api_calls import db_get -from command_logic.logic_gameplay import advance_runners, bunts, chaos, complete_game, doubles, flyballs, get_full_roster_from_sheets, get_lineups_from_sheets, checks_log_interaction, complete_play, get_scorebug_embed, hit_by_pitch, homeruns, is_game_over, manual_end_game, popouts, read_lineup, show_defense_cards, singles, starting_pitcher_dropdown_view, steals, strikeouts, triples, undo_play, update_game_settings, walks, xchecks +from command_logic.logic_gameplay import advance_runners, bunts, chaos, complete_game, doubles, flyballs, get_full_roster_from_sheets, get_lineups_from_sheets, checks_log_interaction, complete_play, get_scorebug_embed, groundballs, hit_by_pitch, homeruns, is_game_over, manual_end_game, popouts, read_lineup, show_defense_cards, singles, starting_pitcher_dropdown_view, steals, strikeouts, triples, undo_play, update_game_settings, walks, xchecks from dice import ab_roll from exceptions import GameNotFoundException, GoogleSheetsException, TeamNotFoundException, PlayNotFoundException, GameException, log_exception from helpers import DEFENSE_LITERAL, PD_PLAYERS_ROLE_NAME, get_channel, team_role, user_has_role, random_gif, random_from_list @@ -88,7 +88,7 @@ class Gameplay(commands.Cog): if this_play.game.roll_buttons and interaction.user.id in [this_play.game.away_team.gmid, this_play.game.home_team.gmid]: scorebug_buttons = ScorebugButtons(this_play, scorebug_embed) - if this_play.on_base_code == 0 and this_play.game.auto_roll and not this_play.batter.team.is_ai: + if this_play.on_base_code == 0 and this_play.game.auto_roll and not this_play.batter.team.is_ai and not this_play.is_new_inning: this_ab_roll = ab_roll(this_play.batter.team, this_play.game, allow_chaos=False) scorebug_buttons = None @@ -243,6 +243,7 @@ class Gameplay(commands.Cog): True if home_team.is_ai else False, league.value ) + logger.info(f'Chosen SP in Game {this_game.id}: {ai_sp_lineup.player.name_with_desc}') await interaction.edit_original_response( content=f'The {ai_team.sname} are starting **{ai_sp_lineup.player.name_with_desc}**:\n\n{ai_sp_lineup.player.pitcher_card_url}' ) @@ -251,6 +252,7 @@ class Gameplay(commands.Cog): final_message = await interaction.channel.send( content=f'{ai_team.gmname} is filling out the {ai_team.sname} lineup card...' ) + logger.info(f'Pulling lineup in Game {this_game.id}') batter_lineups = await get_starting_lineup( session, team=ai_team, @@ -260,12 +262,14 @@ class Gameplay(commands.Cog): ) # Check for last game settings + logger.info(f'Checking human team\'s automation preferences...') g_query = session.exec(select(Game).where(or_(Game.home_team == human_team, Game.away_team == human_team)).order_by(Game.id.desc()).limit(1)).all() if len(g_query) > 0: last_game = g_query[0] this_game.auto_roll = last_game.auto_roll this_game.roll_buttons = last_game.roll_buttons + logger.info(f'Setting auto_roll to {last_game.auto_roll} and roll_buttons to {last_game.roll_buttons}') # Commit game and lineups session.add(this_game) @@ -517,6 +521,16 @@ class Gameplay(commands.Cog): await self.complete_and_post_play(session, interaction, this_play) + @group_log.command(name='groundball', description='Groundballs: a, b, c') + async def log_groundball(self, interaction: discord.Interaction, groundball_type: Literal['a', 'b', 'c']): + with Session(engine) as session: + this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name=f'log groundball {groundball_type}') + + logger.info(f'log groundball {groundball_type} - this_play: {this_play}') + this_play = await groundballs(session, interaction, this_play, groundball_type) + + await self.complete_and_post_play(session, interaction, this_play) + @group_log.command(name='hit-by-pitch', description='Hit by pitch: batter to first; runners advance if forced') async def log_hit_by_pitch(self, interaction: discord.Interaction): with Session(engine) as session: diff --git a/cogs/owner.py b/cogs/owner.py index 8e95a88..b9f3456 100644 --- a/cogs/owner.py +++ b/cogs/owner.py @@ -90,7 +90,7 @@ class Owner(commands.Cog): @commands.command(name='sync', help='~ current guild, * global -> current, ! clear and sync current') @commands.is_owner() - async def sync(self, ctx: Context, guilds: Greedy[Object], spec: Optional[Literal['~', "*", '!']] = None) -> None: + async def sync(self, ctx: Context, guilds: Greedy[Object], spec: Optional[Literal['~', "*", '!', '^']] = None) -> None: """ !sync This takes all global commands within the CommandTree and sends them to Discord. (see CommandTree for more info.) diff --git a/command_logic/logic_gameplay.py b/command_logic/logic_gameplay.py index 52fdb21..f6ef225 100644 --- a/command_logic/logic_gameplay.py +++ b/command_logic/logic_gameplay.py @@ -16,6 +16,7 @@ from helpers import DEFENSE_LITERAL, SBA_COLOR, get_channel from in_game.game_helpers import legal_check from in_game.gameplay_models import BattingCard, Game, Lineup, PositionRating, RosterLink, Team, Play from in_game.gameplay_queries import 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 from utilities.dropdown import DropdownView, SelectStartingPitcher, SelectViewDefense from utilities.embeds import image_embed @@ -263,7 +264,7 @@ async def read_lineup(session: Session, interaction: discord.Interaction, this_g session.add(this_game) - human_lineups = await get_lineups_from_sheets(session, sheets_auth, this_game, this_team=lineup_team, lineup_num=lineup.name, roster_num=this_game.away_roster_id if this_game.is_ai else this_game.home_roster_id) + human_lineups = await get_lineups_from_sheets(session, sheets_auth, this_game, this_team=lineup_team, lineup_num=lineup.name, 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...') @@ -637,6 +638,7 @@ async def checks_log_interaction(session: Session, interaction: discord.Interact this_play.locked = True session.add(this_play) session.commit() + session.refresh(this_play) return this_game, owner_team, this_play @@ -664,12 +666,19 @@ def log_run_scored(session: Session, runner: Lineup, this_play: Play, is_earned: return True -def advance_runners(session: Session, this_play: Play, num_bases: int, is_error: bool = False, only_forced: bool = False) -> Play: +def advance_runners(session: Session, this_play: Play, num_bases: int, only_forced: bool = False, earned_bases: int = None) -> Play: """ No commits """ logger.info(f'Advancing runners {num_bases} bases in game {this_play.game.id}') + if earned_bases is None: + earned_bases = num_bases this_play.rbi = 0 + er_from = { + 3: True if earned_bases >= 1 else False, + 2: True if earned_bases >= 2 else False, + 1: True if earned_bases >= 3 else False + } if num_bases == 0: if this_play.on_first is not None: @@ -690,13 +699,13 @@ def advance_runners(session: Session, this_play: Play, num_bases: int, is_error: 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 + 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) - this_play.rbi += 1 if not is_error else 0 + log_run_scored(session, this_play.on_second, this_play, is_earned=er_from[2]) + this_play.rbi += 1 if er_from[2] else 0 elif num_bases == 1: this_play.on_second_final = 3 else: @@ -707,8 +716,8 @@ def advance_runners(session: Session, this_play: Play, num_bases: int, is_error: 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 + log_run_scored(session, this_play.on_first, this_play, is_earned=er_from[1]) + this_play.rbi += 1 if er_from[1] else 0 elif num_bases == 2: this_play.on_first_final = 3 elif num_bases == 1: @@ -720,16 +729,16 @@ def advance_runners(session: Session, this_play: Play, num_bases: int, is_error: 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 + log_run_scored(session, this_play.on_third, this_play, is_earned=er_from[3]) + this_play.rbi += 1 if er_from[3] else 0 else: this_play.on_third_final = 3 if this_play.on_second: if num_bases > 1: this_play.on_second_final = 4 - log_run_scored(session, this_play.on_second, this_play) - this_play.rbi += 1 if not is_error else 0 + log_run_scored(session, this_play.on_second, this_play, is_earned=er_from[2]) + this_play.rbi += 1 if er_from[2] else 0 elif num_bases == 1: this_play.on_second_final = 3 else: @@ -738,8 +747,8 @@ def advance_runners(session: Session, this_play: Play, num_bases: int, is_error: 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 + log_run_scored(session, this_play.on_first, this_play, is_earned=er_from[1]) + this_play.rbi += 1 if er_from[1] else 0 elif num_bases == 2: this_play.on_first_final = 3 elif num_bases == 1: @@ -842,7 +851,7 @@ async def flyballs(session: Session, interaction: discord.Interaction, this_play 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) + this_play = advance_runners(session, this_play, num_bases=1) if this_play.on_third: this_play.ab = 0 @@ -850,7 +859,7 @@ async def flyballs(session: Session, interaction: discord.Interaction, this_play 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) + this_play = advance_runners(session, this_play, num_bases=0) if this_play.starting_outs < 2 and this_play.on_third: this_play.ab = 0 @@ -951,7 +960,7 @@ async def flyballs(session: Session, interaction: discord.Interaction, this_play elif flyball_type == 'c': this_play.pa, this_play.ab, this_play.outs = 1, 1, 1 - advance_runners(session, this_play, num_bases=0) + this_play = advance_runners(session, this_play, num_bases=0) session.add(this_play) session.commit() @@ -1238,14 +1247,14 @@ async def singles(session: Session, interaction: discord.Interaction, this_play: this_play.hit, this_play.batter_final = 1, 1 if single_type == '**': - advance_runners(session, this_play, num_bases=2) + this_play = advance_runners(session, this_play, num_bases=2) elif single_type in ['*', 'ballpark']: - advance_runners(session, this_play, num_bases=1) + this_play = 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) + this_play = advance_runners(session, this_play, 1) if this_play.on_base_code in [1, 2, 4, 5, 6, 7]: if this_play.on_second: @@ -1535,8 +1544,10 @@ async def chaos(session: Session, interaction: discord.Interaction, this_play: P 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: - advance_runners(session, this_play, 0) + this_play = advance_runners(session, this_play, 0) + this_play.pa = 0 if steal_type in ['stolen-base', 'steal-plus-overthrow']: this_play.sb = 1 @@ -1602,7 +1613,8 @@ async def steals(session: Session, interaction: discord.Interaction, this_play: session.refresh(this_play) return this_play -async def xchecks(session: Session, interaction: discord.Interaction, this_play: Play, position: str) -> 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, @@ -1610,43 +1622,72 @@ async def xchecks(session: Session, interaction: discord.Interaction, this_play: this_team=defense_team, position=position ) + this_play.defender = this_defender + this_play.check_pos = position + def_alignment = this_play.managerai.defense_alignment(session, this_play.game) + defender_is_in = def_alignment.defender_in(position) + playing_in = False defender_embed = defense_team.embed defender_embed.title = f'{defense_team.sname} {position} Check' defender_embed.description = f'{this_defender.player.name}' defender_embed.set_image(url=this_defender.player.image) - embeds = [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}') + + if this_play.on_third is not None: + if not this_play.ai_is_batting and defender_is_in: + playing_in = True + + elif this_play.ai_is_batting: + playing_in = await ask_confirm( + interaction, + question=f'Was {this_defender.card.player.name} playing in?', + label_type='yes' + ) + + if playing_in: + this_rating.range = min(this_rating.range + 1, 5) + this_roll = sa_fielding_roll(defense_team, this_play, position, this_rating) + logger.info(f'this_roll: {this_roll}') - question = f'Looks like this is a {this_roll.hit_result}' - if this_roll.is_chaos: - question += ' **rare play**.' - elif this_roll.error_result is not None: - question += f' plus {this_roll.error_result}-base error.' - question += f'Is that correct?' + if not debug: + question = f'Looks like this is a **{this_roll.hit_result}**' + if this_roll.is_chaos: + question += ' **rare play**' + elif this_roll.error_result is not None: + question += f' plus {this_roll.error_result}-base error' + question += f'. Is that correct?' - await interaction.edit_original_response( - content=None, - embeds=this_roll.embeds - ) - is_correct = await ask_confirm( - interaction, - question, - label_type='yes', - timeout=30, - ) + await interaction.edit_original_response( + content=None, + embeds=[defender_embed, *this_roll.embeds] + ) + is_correct = await ask_confirm( + interaction, + question, + label_type='yes', + timeout=30, + ) + else: + is_correct = True hit_result = this_roll.hit_result error_result = this_roll.error_result is_rare_play = this_roll.is_chaos + logger.info(f'X-Check in Game #{this_play.game_id} at {this_play.check_pos} for {this_play.defender.card.player.name_with_desc} of the {this_play.pitcher.team.sname} / hit_result: {hit_result} / error_result: {error_result} / is_correct: {is_correct}') + if not is_correct: # Full questionnaire pass - if hit_result == 'SPD': + if hit_result == 'SPD' and not is_rare_play: is_out = ask_confirm( interaction, f'Is {this_play.batter.player.name} thrown out at first?', @@ -1657,14 +1698,284 @@ async def xchecks(session: Session, interaction: discord.Interaction, this_play: hit_result = 'G3' else: this_play = await singles(session, interaction, this_play, '*') + + 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 hit_result not in ['SI1', 'SI2', 'DO2', 'DO3', 'TR'] and error_result is None: + elif this_play.on_base_code > 0: + is_holding = False + + if not playing_in: + if position == '1B' and this_play.on_first is not None: + is_holding = await ask_confirm( + interaction, + question=f'Was {this_play.on_second.card.player.name} held at first base?', + label_type='yes' + ) + + elif position == '3B' and this_play.on_second is not None: + is_holding = await ask_confirm( + interaction, + question=f'Was {this_play.on_second.card.player.name} held at second base?', + label_type='yes' + ) + + elif position == '2B' and (this_play.on_first is not None or this_play.on_second is not None) and (this_play.batter.card.batterscouting.battingcard.hand == 'R' or (this_play.batter.card.batterscouting.battingcard.hand == 'S' and this_play.pitcher.card.pitcherscouting.pitchingcard.hand == 'L')): + if this_play.on_second is not None: + is_holding = await ask_confirm( + interaction, + question=f'Was {this_play.on_second.card.player.name} held at second base?', + label_type='yes' + ) + + elif this_play.on_first is not None: + is_holding = await ask_confirm( + interaction, + question=f'Was {this_play.on_first.card.player.name} held at first base?', + label_type='yes' + ) + + elif position == 'SS' and (this_play.on_first is not None or this_play.on_second is not None) and (this_play.batter.card.batterscouting.battingcard.hand == 'L' or (this_play.batter.card.batterscouting.battingcard.hand == 'S' and this_play.pitcher.card.pitcherscouting.pitchingcard.hand == 'R')): + if this_play.on_second is not None: + is_holding = await ask_confirm( + interaction, + question=f'Was {this_play.on_second.card.player.name} held at second base?', + label_type='yes' + ) + + elif this_play.on_first is not None: + is_holding = await ask_confirm( + interaction, + question=f'Was {this_play.on_first.card.player.name} held at first base?', + label_type='yes' + ) + + if is_holding or playing_in: + hit_result = 'SI2' + + if is_rare_play: + logger.info(f'Is rare play') + if hit_result == 'SI1': + this_play = await singles(session, interaction, this_play, '*') + if this_play.on_first is None: + this_play.error = 1 + this_play.batter_final = 2 + elif hit_result == 'SI2': + this_play = await singles(session, interaction, this_play, '**') + this_play.batter_final = None + this_play.outs = 1 + + elif 'DO' in hit_result: + this_play = await doubles(session, interaction, this_play, '***') + this_play.batter_final = None + this_play.outs = 1 + + elif hit_result == 'TR': + this_play = await triples(session, interaction, this_play) + this_play.batter_final = 4 + this_play.run = 1 + this_play.error = 1 + + elif hit_result == 'PO': + this_play = advance_runners(session, this_play, 1, earned_bases=0) + this_play.ab, this_play.error, this_play.batter_final = 1, 1, 1 + + elif hit_result == 'FO': + this_play = advance_runners(session, this_play, 1, is_error=True, only_forced=True) + this_play.ab, this_play.error, this_play.batter_final = 1, 1, 1 + + elif hit_result == 'G1': + if this_play.on_first is not None and this_play.starting_outs < 2: + this_play = await gb_letter(session, interaction, this_play, 'B', position=this_play.check_pos, defender_is_in=defender_is_in) + else: + this_play = await gb_letter(session, interaction, this_play, 'A', position=this_play.check_pos, defender_is_in=defender_is_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=defender_is_in) + + else: + this_play = await gb_letter(session, interaction, this_play, 'B', position=this_play.check_pos, defender_is_in=defender_is_in) + + elif hit_result == 'G3': + if this_play.on_base_code > 0: + this_play = await singles(session, interaction, this_play, '*') + else: + this_play = await gb_letter(session, interaction, this_play, 'C', position=this_play.check_pos, defender_is_in=defender_is_in) + + elif hit_result == 'SPD': + this_play = singles(session, interaction, this_play, '*') + + elif hit_result == 'F1': + this_play.outs = 1 + this_play.ab = 1 if this_play.on_third is None else 0 + if this_play.on_base_code > 0 and this_play.starting_outs < 2: + this_play = advance_runners(session, this_play, 1) + + if this_play.on_second is not None: + this_play.on_second_final = 4 + log_run_scored(session, this_play.on_second, this_play, is_earned=False) + + elif this_play.on_first is not None: + this_play.on_first_final = 3 + + elif hit_result == 'F2': + this_play.outs = 1 + this_play.ab = 1 if this_play.on_third is None else 0 + + if this_play.on_base_code > 0 and this_play.starting_outs < 2: + this_play.on_third_final = None + this_play.outs = 2 + + else: + this_play.outs = 1 + this_play.ab = 1 + + if this_play.on_third: + this_play.outs = 2 + this_play.on_third_final = None + + elif this_play.on_second: + this_play.outs = 2 + this_play.on_second_final = None + + elif this_play.on_first: + this_play.outs = 2 + this_play.on_first_final = None + + elif hit_result not in ['SI1', 'SI2', 'DO2', 'DO3', 'TR'] and error_result is None: + logger.info(f'Not a hit, not an error') if this_play.on_base_code == 0: - this_play.ab, this_play.outs = 1, 1 + this_play = await gb_result(session, interaction, this_play, 1) - # TODO: Continue working through results - # elif this_play + 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 defender_is_in: + this_play = await gb_result(session, interaction, this_play, 12) + + elif defender_is_in and this_play.on_base_code == 5: + this_play = await gb_result(session, interaction, this_play, 7, to_mif, to_right_side) + + elif defender_is_in and this_play.on_base_code in [3, 6]: + this_play = await gb_decide(session, interaction=interaction, this_play=this_play) + + elif defender_is_in and this_play.on_base_code == 7: + this_play = await gb_result(session, interaction, this_play, 11) + + else: + this_play = await gb_result(session, interaction, this_play, 3) + + elif 'G2' in hit_result: + if this_play.on_base_code == 7 and defender_is_in: + this_play = await gb_result(session, interaction, this_play, 11) + + elif not defender_is_in and this_play.on_base_code in [3, 6]: + this_play = await gb_result(session, interaction, this_play, 5, to_mif=to_mif) + + elif defender_is_in and this_play.on_base_code in [3, 5, 6]: + this_play = await gb_result(session, interaction, this_play, 1) + + elif this_play.on_base_code == 2: + this_play = await gb_result(session, interaction, this_play, 12) + + else: + this_play = await gb_result(session, interaction, this_play, 4) + + elif 'G1' in hit_result: + if this_play.on_base_code == 7 and defender_is_in: + this_play = await gb_result(session, interaction, this_play, 10) + + elif not defender_is_in and this_play.on_base_code == 4: + this_play = await gb_result(session, interaction, this_play, 13) + + elif not defender_is_in and this_play.on_base_code in [3, 6]: + this_play = await gb_result(session, interaction, this_play, 3) + + elif defender_is_in and this_play.on_base_code in [3, 5, 6]: + this_play = await gb_result(session, interaction, this_play, 1) + + elif this_play.on_base_code == 2: + this_play = await gb_result(session, interaction, this_play, 12) + + else: + this_play = await gb_result(session, interaction, this_play, 2) + + elif 'F1' in hit_result: + this_play = await flyballs(session, interaction, this_play, 'a') + + elif 'F2' in hit_result: + this_play = await flyballs(session, interaction, this_play, 'b') + + elif 'F3' in hit_result: + this_play = await flyballs(session, interaction, this_play, 'c') + + # FO and PO + else: + this_play.ab, this_play.outs = 1, 1 + this_play = advance_runners(session, this_play, 0) + + elif hit_result not in ['SI1', 'SI2', 'DO2', 'DO3', 'TR'] and error_result is not None: + logger.info(f'Not a hit, {error_result}-base error') + this_play = advance_runners(session, this_play, error_result, earned_bases=0) + this_play.ab, this_play.error, this_play.batter_final = 1, 1, error_result + + else: + logger.info(f'Hit result: {hit_result}, Error: {error_result}') + if hit_result == 'SI1' and error_result is None: + this_play = await singles(session, interaction, this_play, '*') + elif hit_result == 'SI1': + this_play.ab, this_play.hit, this_play.error, this_play.batter_final = 1, 1, 1, 2 + this_play = advance_runners(session, this_play, num_bases=error_result + 1, earned_bases=1) + + elif hit_result == 'SI2' and error_result is None: + this_play = await singles(session, interaction, this_play, '**') + elif hit_result == 'SI2': + this_play.ab, this_play.hit, this_play.error = 1, 1, 1 + if error_result > 1: + num_bases = 3 + this_play.batter_final = 3 + else: + num_bases = 2 + this_play.batter_final = 2 + this_play = advance_runners(session, this_play, num_bases=num_bases, earned_bases=2) + + elif hit_result == 'DO2' and error_result is None: + this_play = await doubles(session, interaction, this_play, '**') + elif hit_result == 'DO2': + this_play.ab, this_play.hit, this_play.error, this_play.double = 1, 1, 1, 1 + num_bases = 3 + + if error_result == 3: + this_play.batter_final = 4 + else: + this_play.batter_final = 3 + + this_play = advance_runners(session, this_play, num_bases=num_bases, earned_bases=2) + + elif hit_result == 'DO3' and error_result is None: + this_play = await doubles(session, interaction, this_play, '***') + elif hit_result == 'DO3': + this_play.ab, this_play.hit, this_play.error, this_play.double = 1, 1, 1, 1 + + if error_result == 1: + this_play.batter_final = 3 + else: + this_play.batter_final = 4 + + this_play = advance_runners(session, this_play, num_bases=4, earned_bases=2) + + elif hit_result == 'TR' and error_result is None: + this_play = await triples(session, interaction, this_play) + else: + this_play.ab, this_play.hit, this_play.error, this_play.triple = 1, 1, 1, 1 + this_play = advance_runners(session, this_play, num_bases=4, earned_bases=3) session.add(this_play) session.commit() @@ -1672,6 +1983,7 @@ async def xchecks(session: Session, interaction: discord.Interaction, this_play: 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() @@ -2197,10 +2509,84 @@ async def manual_end_game(session: Session, interaction: discord.Interaction, th await complete_game(session, interaction, current_play) -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 groundballs(session: Session, interaction: discord.Interaction, this_play: Play, groundball_letter: Literal['a', 'b', 'c']): + if this_play.on_base_code == 2 and groundball_letter in ['a', 'b']: + to_right_side = await ask_confirm( + interaction, + question=f'Was that ball hit to either 1B or 2B?', + label_type='yes' + ) + this_play = gb_result_6(session, this_play, to_right_side) + + elif this_play.on_base_code in [3, 6] and groundball_letter in ['a', 'b']: + to_mif = await ask_confirm( + interaction, + question=f'Was that ball hit to either 2B or SS?', + label_type='yes' + ) + this_play = gb_result_5(session, this_play, to_mif) + + else: + this_play = await gb_letter(session, interaction, this_play, groundball_letter.upper(), 'None', False) + + session.add(this_play) + session.commit() + + session.refresh(this_play) + return this_play + + +async def gb_letter(session: Session, interaction: discord.Interaction, this_play: Play, groundball_letter: Literal['A', 'B', 'C'], position: str, defender_is_in: bool): """ Commits this_play """ + if not defender_is_in: + if this_play.on_base_code == 0: + return await gb_result(session, interaction, this_play, 1) + + elif groundball_letter == 'C': + return await gb_result(session, interaction, this_play, 3) + + elif groundball_letter == 'A' and this_play.on_base_code in [1, 4, 5, 7]: + return await gb_result(session, interaction, this_play, 2) + + elif groundball_letter == 'B' and this_play.on_base_code in [1, 4, 5, 7]: + return await gb_result(session, interaction, this_play, 4) + + elif this_play.on_base_code in [3, 6]: + return await gb_result(session, interaction, this_play, 5, to_mif=position in ['2B', 'SS']) + + else: + return await gb_result(session, interaction, this_play, 6, to_right_side=position in ['1B', '2B']) + + else: + if groundball_letter == 'A' and this_play.on_base_code == 7: + return await gb_result(session, interaction, this_play, 10) + + elif groundball_letter == 'B' and this_play.on_base_code == 5: + return await gb_result(session, interaction, this_play, 9) + + elif this_play.on_base_code == 7: + return await gb_result(session, interaction, this_play, 11) + + elif groundball_letter == 'A': + return await gb_result(session, interaction, this_play, 7) + + elif groundball_letter == 'B': + return await gb_result(session, interaction, this_play, 1) + + else: + return await gb_result(session, interaction, this_play, 8) + + +async def gb_result(session: Session, interaction: discord.Interaction, this_play: Play, groundball_result: int, to_mif: bool = None, to_right_side: bool = None): + """ + Commits this_play + + Result 5 requires to_mif + Result 6 requires to_right_side + """ + logger.info(f'Starting a groundball result: GB #{groundball_result}, to_mif: {to_mif}, to_right_side: {to_right_side}') if groundball_result == 1: this_play = gb_result_1(session, this_play) elif groundball_result == 2: @@ -2224,7 +2610,7 @@ async def gb_result(session: Session, interaction: discord.Interaction, this_pla elif groundball_result == 11: this_play = gb_result_11(session, this_play) elif groundball_result == 12: - this_play = gb_result_12(session, this_play, interaction) + this_play = await gb_result_12(session, this_play, interaction) elif groundball_result == 13: this_play = gb_result_13(session, this_play) @@ -2238,7 +2624,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') this_play = advance_runners(session, this_play, 0) - this_play.ab, this_play.ab, this_play.outs = 1, 1, 1 + this_play.ab, this_play.outs = 1, 1 return this_play @@ -2247,7 +2633,7 @@ def gb_result_2(session: Session, this_play: Play): logger.info(f'GB 2') num_outs = 2 if this_play.starting_outs <= 1 else 1 - this_play.ab, this_play.outs = 1, 1 + this_play.ab, this_play.outs = 1, num_outs this_play.on_first_final = None if num_outs + this_play.starting_outs < 3: @@ -2275,7 +2661,7 @@ def gb_result_4(session: Session, this_play: Play): this_play = advance_runners(session, this_play, 1) this_play.ab, this_play.outs = 1, 1 - this_play.on_first_final = None + this_play.on_first_final = 1 return this_play @@ -2345,14 +2731,17 @@ def gb_result_11(session: Session, this_play: Play): async def gb_decide(session: Session, this_play: Play, interaction: discord.Interaction): logger.info(f'GB Decide') runner = this_play.on_third if this_play.on_third is not None else this_play.on_second + logger.info(f'runner: {runner}') pos_rating = await get_position(session, this_play.defender.card, this_play.check_pos) safe_range = runner.card.batterscouting.battingcard.running - 4 + pos_rating.range + advance_base = 4 if this_play.on_third is not None else 3 + logger.info(f'pos_rating: {pos_rating}\nsafe_range: {safe_range}\nadvance_base: {advance_base}') if this_play.game.ai_team is not None and this_play.ai_is_batting: run_resp = this_play.managerai.gb_decide_run(session, this_play.game) - if this_play.on_second is None: - log_exception(InvalidResultException, 'Cannot run GB Decide without a runner on second.') + if this_play.on_second is None and this_play.on_third is None: + log_exception(InvalidResultException, 'Cannot run GB Decide without a runner on base.') if safe_range >= run_resp.min_safe: is_lead_running = True @@ -2362,7 +2751,7 @@ async def gb_decide(session: Session, this_play: Play, interaction: discord.Inte else: is_lead_running = await ask_confirm( interaction, - f'Is {runner.card.player.name} attempting to advance to third?', + 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 ) @@ -2381,7 +2770,7 @@ async def gb_decide(session: Session, this_play: Play, interaction: discord.Inte ) 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( diff --git a/dice.py b/dice.py index 9197f9a..d555525 100644 --- a/dice.py +++ b/dice.py @@ -32,7 +32,7 @@ class JumpRoll(DiceRoll): class FieldingRoll(DiceRoll): d_six_three: int | None = None - hit_result: Literal['SI1', 'SI2', 'DO2', 'DO3', 'TR', 'G1', 'G2', 'G3', 'G1#', 'G2#', 'G3#', 'F1', 'F2', 'F3', 'SPD'] | None = None + hit_result: Literal['SI1', 'SI2', 'DO2', 'DO3', 'TR', 'G1', 'G2', 'G3', 'G1#', 'G2#', 'G3#', 'F1', 'F2', 'F3', 'SPD', 'PO', 'FO'] | None = None error_result: Literal[1, 2, 3] | None = None @@ -817,6 +817,9 @@ def sa_fielding_roll(this_team: Team, this_play: Play, pos_code: str, def_rating d_twenty = random.randint(1, 20) ) error_dice = this_roll.d_six_one + this_roll.d_six_two + this_roll.d_six_three + this_roll.roll_message = f'```md\n' \ + f'# {str(this_roll.d_twenty)},{str(this_roll.d_six_one + this_roll.d_six_two + this_roll.d_six_three)}\n' \ + f'Details:[1d20;3d6 ({this_roll.d_twenty} - {this_roll.d_six_one} {this_roll.d_six_two} {this_roll.d_six_three})]```' if pos_code in ['1B', '2B', '3B', 'SS']: x_chart = 'https://sombaseball.ddns.net/static/images/season04/range-infield.png' @@ -1632,7 +1635,7 @@ def sa_fielding_roll(this_team: Team, this_play: Play, pos_code: str, def_rating elif def_rating.range == 3: this_roll.hit_result = 'SI2' elif def_rating.range == 4: - this_roll.hit_result = 'DO2' + this_roll.hit_result = 'SI2' else: this_roll.hit_result = 'DO2' @@ -2720,17 +2723,19 @@ def sa_fielding_roll(this_team: Team, this_play: Play, pos_code: str, def_rating ) reference_string = f'[Range Chart]({x_chart}) / [Error Chart]({error_chart}) / [Result Reference]({symbol_link})' chart_embed.add_field(name='References', value=reference_string, inline=False) + + this_roll.embeds = [roll_embed, chart_embed] - error1_embed, error2_embed = None, None if error_note1: error1_embed = get_dice_embed(this_team) error1_embed.add_field(name='Error Results', value=error_note1) + this_roll.embeds.append(error1_embed) if error_note2: error2_embed = get_dice_embed(this_team) error2_embed.add_field(name='Error Results', value=error_note2) - - this_roll.embeds = [roll_embed, chart_embed, error1_embed, error2_embed] - logger.info(f'Game {this_play.game.id} | Team {this_team.id} ({this_team.abbrev}): {this_roll.roll_message}') + this_roll.embeds.append(error1_embed) + + logger.info(f'Game {this_play.game.id} | Team {this_team.id} ({this_team.abbrev}) Roll Message: {this_roll.roll_message}') return this_roll @@ -2778,21 +2783,20 @@ def ab_roll(this_team: Team, this_game: Game, allow_chaos: bool = True) -> AbRol if allow_chaos: if d_twenty == 1: + logger.info(f'Game {this_game.id} - Wild Pitch Check') flag = 'wild pitch' elif d_twenty == 2: if random.randint(1, 2) == 1: + logger.info(f'Game {this_game.id} - Balk Check') flag = 'balk' else: + logger.info(f'Game {this_game.id} - Passed Ball Check') flag = 'passed ball' if flag: roll_message = f'```md\nCheck {flag}```\n' \ f'{flag.title()} roll```md\n# {d_twenty_two}\nDetails: [1d20 ({d_twenty_two})]```\n' embed = get_dice_embed(this_team, f'Chaos roll for the {this_team.sname}', roll_message) - embed.set_footer( - text='If the chaos roll fails, ignore future chaos until a new batter comes to the plate.' - ) - # return {'is_chaos': True, 'embeds': [embed]} return AbRoll( roll_message=roll_message, is_chaos=True, diff --git a/in_game/gameplay_models.py b/in_game/gameplay_models.py index a64ab24..9c5c628 100644 --- a/in_game/gameplay_models.py +++ b/in_game/gameplay_models.py @@ -658,38 +658,55 @@ class ManagerAi(ManagerAiBase, table=True): ai_rd = this_play.ai_run_diff aggression = self.ahead_aggression - 5 if ai_rd > 0 else self.behind_aggression - 5 + pitcher_hold = this_play.pitcher.card.pitcherscouting.pitchingcard.hold + + catcher_defense = session.exec(select(PositionRating).where(PositionRating.player_id == this_play.catcher.player_id, PositionRating.position == 'C', PositionRating.variant == this_play.catcher.card.variant)).one() + catcher_hold = catcher_defense.arm + battery_hold = pitcher_hold + catcher_hold - if self.starting_outs == 2 and self.on_base_code > 0: - if self.on_base_code == 1: + if this_play.starting_outs == 2 and this_play.on_base_code > 0: + if this_play.on_base_code == 1: this_resp.hold_first = True - elif self.on_base_code == 2: + this_resp.ai_note += f'- hold {this_play.on_first.player.name} on 1st\n' + elif this_play.on_base_code == 2: this_resp.hold_second = True - elif self.on_base_code in [4, 5, 7]: + this_resp.ai_note += f'- hold {this_play.on_second.player.name} on 2nd\n' + elif this_play.on_base_code in [4, 5, 7]: this_resp.hold_first = True this_resp.hold_second = True + this_resp.ai_note += f'- hold {this_play.on_first.player.name} on 1st\n- hold {this_play.on_second.player.name} on 2nd\n' # elif self.on_base_code == 5: - # ai_note += f'- hold the runner on first\n' - elif self.on_base_code == 6: - ai_note += f'- hold {self.on_second.player.name} on 2nd\n' - elif self.on_base_code in [1, 5]: - runner = self.on_first.player - if self.on_first.card.batterscouting.battingcard.steal_auto: - ai_note += f'- hold {runner.name} on 1st\n' - elif self.on_base_code in [2, 4]: - if self.on_second.card.batterscouting.battingcard.steal_low + max(self.pitcher.card.pitcherscouting.pitchingcard.hold, 5) >= 14: - ai_note += f'- hold {self.on_second.player.name} on 2nd\n' + # this_resp.ai_note += f'- hold the runner on first\n' + elif this_play.on_base_code == 6: + this_resp.hold_second = True + this_resp.ai_note += f'- hold {this_play.on_second.player.name} on 2nd\n' + elif this_play.on_base_code in [1, 5]: + runner = this_play.on_first.player + if this_play.on_first.card.batterscouting.battingcard.steal_auto and ((this_play.on_first.card.batterscouting.battingcard.steal_high + battery_hold) >= (12 - aggression)): + this_resp.hold_first = True + this_resp.ai_note += f'- hold {runner.name} on 1st\n' + elif this_play.on_base_code in [2, 4]: + if (this_play.on_second.card.batterscouting.battingcard.steal_low + max(battery_hold, 5)) >= (14 - aggression): + this_resp.hold_second = True + this_resp.ai_note += f'- hold {this_play.on_second.player.name} on 2nd\n' # Defensive Alignment - if self.on_third and self.starting_outs < 2: - if self.could_walkoff: - ai_note += f'- play the outfield and infield in' - elif abs(self.away_score - self.home_score) <= 3: - ai_note += f'- play the whole infield in\n' + if this_play.on_third and this_play.starting_outs < 2: + if this_play.could_walkoff: + this_resp.outfield_in = True + this_resp.infield_in = True + this_resp.ai_note += f'- play the outfield and infield in' + elif abs(this_play.away_score - this_play.home_score) <= 3: + this_resp.infield_in = True + this_resp.ai_note += f'- play the whole infield in\n' else: - ai_note += f'- play the corners in\n' + this_resp.corners_in = True + this_resp.ai_note += f'- play the corners in\n' - if len(ai_note) == 0 and self.on_base_code > 0: - ai_note += f'- play straight up\n' + if len(this_resp.ai_note) == 0 and this_play.on_base_code > 0: + this_resp.ai_note += f'- play straight up\n' + + return this_resp def gb_decide_run(self, session: Session, this_game: Game) -> RunResponse: this_resp = RunResponse() @@ -881,21 +898,21 @@ class BattingRatings(BattingRatingsBase, table=True): class BatterScoutingBase(SQLModel): id: int | None = Field(default=None, primary_key=True) - battingcard_id: int | None = Field(default=None, foreign_key='battingcard.id') - ratings_vl_id: int | None = Field(default=None, foreign_key='battingratings.id') - ratings_vr_id: int | None = Field(default=None, foreign_key='battingratings.id') + battingcard_id: int | None = Field(default=None, foreign_key='battingcard.id', ondelete='CASCADE') + ratings_vl_id: int | None = Field(default=None, foreign_key='battingratings.id', ondelete='CASCADE') + ratings_vr_id: int | None = Field(default=None, foreign_key='battingratings.id', ondelete='CASCADE') created: datetime.datetime = Field(default_factory=datetime.datetime.now, nullable=True) class BatterScouting(BatterScoutingBase, table=True): battingcard: BattingCard = Relationship() #back_populates='batterscouting') ratings_vl: BattingRatings = Relationship( - sa_relationship_kwargs=dict(foreign_keys="[BatterScouting.ratings_vl_id]") + sa_relationship_kwargs=dict(foreign_keys="[BatterScouting.ratings_vl_id]",single_parent=True), cascade_delete=True ) ratings_vr: BattingRatings = Relationship( - sa_relationship_kwargs=dict(foreign_keys="[BatterScouting.ratings_vr_id]") + sa_relationship_kwargs=dict(foreign_keys="[BatterScouting.ratings_vr_id]",single_parent=True), cascade_delete=True ) - cards: list['Card'] = Relationship(back_populates='batterscouting', cascade_delete=True) + cards: list['Card'] = Relationship(back_populates='batterscouting') class PitchingCardBase(SQLModel): @@ -962,21 +979,21 @@ class PitchingRatings(PitchingRatingsBase, table=True): class PitcherScoutingBase(SQLModel): id: int | None = Field(default=None, primary_key=True) - pitchingcard_id: int | None = Field(default=None, foreign_key='pitchingcard.id',) - ratings_vl_id: int | None = Field(default=None, foreign_key='pitchingratings.id') - ratings_vr_id: int | None = Field(default=None, foreign_key='pitchingratings.id') + pitchingcard_id: int | None = Field(default=None, foreign_key='pitchingcard.id', ondelete='CASCADE') + ratings_vl_id: int | None = Field(default=None, foreign_key='pitchingratings.id', ondelete='CASCADE') + ratings_vr_id: int | None = Field(default=None, foreign_key='pitchingratings.id', ondelete='CASCADE') created: datetime.datetime = Field(default_factory=datetime.datetime.now, nullable=True) class PitcherScouting(PitcherScoutingBase, table=True): pitchingcard: PitchingCard = Relationship() ratings_vl: PitchingRatings = Relationship( - sa_relationship_kwargs=dict(foreign_keys="[PitcherScouting.ratings_vl_id]") + sa_relationship_kwargs=dict(foreign_keys="[PitcherScouting.ratings_vl_id]",single_parent=True), cascade_delete=True ) ratings_vr: PitchingRatings = Relationship( - sa_relationship_kwargs=dict(foreign_keys="[PitcherScouting.ratings_vr_id]") + sa_relationship_kwargs=dict(foreign_keys="[PitcherScouting.ratings_vr_id]",single_parent=True), cascade_delete=True ) - cards: list['Card'] = Relationship(back_populates='pitcherscouting', cascade_delete=True) + cards: list['Card'] = Relationship(back_populates='pitcherscouting') class CardBase(SQLModel): diff --git a/in_game/gameplay_queries.py b/in_game/gameplay_queries.py index 1362d3c..3717c90 100644 --- a/in_game/gameplay_queries.py +++ b/in_game/gameplay_queries.py @@ -155,18 +155,35 @@ async def get_player_or_none(session: Session, player_id: int, skip_cache: bool async def get_batter_scouting_or_none(session: Session, card: Card, skip_cache: bool = False) -> BatterScouting | None: logger.info(f'Getting batting scouting for card ID #{card.id}: {card.player.name_with_desc}') - if not skip_cache and card.batterscouting is not None: - this_scouting = session.get(BatterScouting, card.batterscouting.id) - if this_scouting is not None: - logger.info(f'we found a cached scouting: {this_scouting} / created {this_scouting.created}') - tdelta = datetime.datetime.now() - this_scouting.created - logger.debug(f'tdelta: {tdelta}') - if tdelta.total_seconds() < CACHE_LIMIT: - return this_scouting - else: - session.delete(this_scouting) - session.commit() + s_query = await db_get(f'battingcardratings/player/{card.player.id}', none_okay=False) + if s_query['count'] != 2: + log_exception(DatabaseError, f'Scouting for {card.player.name_with_desc} was not found.') + + this_scouting = session.get(BattingCard, s_query['ratings'][0]['battingcard']['id']) + + if this_scouting is not None: + logger.info(f'we found a cached scouting: {this_scouting} / created {this_scouting.created}') + tdelta = datetime.datetime.now() - this_scouting.created + logger.debug(f'tdelta: {tdelta}') + if tdelta.total_seconds() < CACHE_LIMIT: + return this_scouting + else: + session.delete(this_scouting) + session.commit() + + # if not skip_cache and card.batterscouting is not None: + # this_scouting = session.get(BatterScouting, card.batterscouting.id) + + # if this_scouting is not None: + # logger.info(f'we found a cached scouting: {this_scouting} / created {this_scouting.created}') + # tdelta = datetime.datetime.now() - this_scouting.created + # logger.debug(f'tdelta: {tdelta}') + # if tdelta.total_seconds() < CACHE_LIMIT: + # return this_scouting + # else: + # session.delete(this_scouting) + # session.commit() def cache_scouting(batting_card: dict, ratings_vr: dict, ratings_vl: dict) -> BatterScouting: valid_bc = BattingCardBase.model_validate(batting_card, from_attributes=True) @@ -188,33 +205,45 @@ async def get_batter_scouting_or_none(session: Session, card: Card, skip_cache: session.commit() session.refresh(db_scouting) return db_scouting + + return cache_scouting( + batting_card=s_query['ratings'][0]['battingcard'], + ratings_vr=s_query['ratings'][0] if s_query['ratings'][0]['vs_hand'] == 'R' else s_query['ratings'][1], + ratings_vl=s_query['ratings'][0] if s_query['ratings'][0]['vs_hand'] == 'L' else s_query['ratings'][1] + ) - s_query = await db_get(f'battingcardratings/player/{card.player.id}', none_okay=False) - if s_query['count'] == 2: - return cache_scouting( - batting_card=s_query['ratings'][0]['battingcard'], - ratings_vr=s_query['ratings'][0] if s_query['ratings'][0]['vs_hand'] == 'R' else s_query['ratings'][1], - ratings_vl=s_query['ratings'][0] if s_query['ratings'][0]['vs_hand'] == 'L' else s_query['ratings'][1] - ) + # s_query = await db_get(f'battingcardratings/player/{card.player.id}', none_okay=False) + # if s_query['count'] == 2: + # return cache_scouting( + # batting_card=s_query['ratings'][0]['battingcard'], + # ratings_vr=s_query['ratings'][0] if s_query['ratings'][0]['vs_hand'] == 'R' else s_query['ratings'][1], + # ratings_vl=s_query['ratings'][0] if s_query['ratings'][0]['vs_hand'] == 'L' else s_query['ratings'][1] + # ) - return None + # return None async def get_pitcher_scouting_or_none(session: Session, card: Card, skip_cache: bool = False) -> PitcherScouting | None: logger.info(f'Getting pitching scouting for card ID #{card.id}: {card.player.name_with_desc}') - if not skip_cache and card.pitcherscouting is not None: - this_scouting = session.get(PitcherScouting, card.pitcherscouting.id) + + s_query = await db_get(f'pitchingcardratings/player/{card.player.id}', none_okay=False) + if s_query['count'] != 2: + log_exception(DatabaseError, f'Scouting for {card.player.name_with_desc} was not found.') - if this_scouting is not None: - logger.info(f'we found a cached scouting: {this_scouting} / created {this_scouting.created}') - tdelta = datetime.datetime.now() - this_scouting.created - logger.debug(f'tdelta: {tdelta}') - if tdelta.total_seconds() < CACHE_LIMIT: - return this_scouting - else: - session.delete(this_scouting) - session.commit() + this_scouting = session.get(PitcherScouting, s_query['ratings'][0]['pitchingcard']['id']) + if this_scouting is not None: + logger.info(f'we found a cached scouting: {this_scouting} / created {this_scouting.created}') + tdelta = datetime.datetime.now() - this_scouting.created + logger.debug(f'tdelta: {tdelta}') + + if tdelta.total_seconds() < CACHE_LIMIT: + return this_scouting + + else: + session.delete(this_scouting) + session.commit() + def cache_scouting(pitching_card: dict, ratings_vr: dict, ratings_vl: dict) -> PitcherScouting: valid_bc = PitchingCardBase.model_validate(pitching_card, from_attributes=True) db_bc = PitchingCard.model_validate(valid_bc) @@ -236,17 +265,60 @@ async def get_pitcher_scouting_or_none(session: Session, card: Card, skip_cache: session.refresh(db_scouting) return db_scouting - s_query = await db_get(f'pitchingcardratings/player/{card.player.id}', none_okay=False) - if s_query['count'] == 2: - scouting = cache_scouting( - pitching_card=s_query['ratings'][0]['pitchingcard'], - ratings_vr=s_query['ratings'][0] if s_query['ratings'][0]['vs_hand'] == 'R' else s_query['ratings'][1], - ratings_vl=s_query['ratings'][0] if s_query['ratings'][0]['vs_hand'] == 'L' else s_query['ratings'][1] - ) - pos_rating = await get_position(session, card, 'P') - return scouting + scouting = cache_scouting( + pitching_card=s_query['ratings'][0]['pitchingcard'], + ratings_vr=s_query['ratings'][0] if s_query['ratings'][0]['vs_hand'] == 'R' else s_query['ratings'][1], + ratings_vl=s_query['ratings'][0] if s_query['ratings'][0]['vs_hand'] == 'L' else s_query['ratings'][1] + ) + pos_rating = await get_position(session, card, 'P') + return scouting - return None + # if not skip_cache and card.pitcherscouting is not None: + # this_scouting = session.get(PitcherScouting, card.pitcherscouting.id) + # # s_query = session.exec(select(PitcherScouting).where(PitcherScouting.pitchingcard_id == card.)) + + # if this_scouting is not None: + # logger.info(f'we found a cached scouting: {this_scouting} / created {this_scouting.created}') + # tdelta = datetime.datetime.now() - this_scouting.created + # logger.debug(f'tdelta: {tdelta}') + # if tdelta.total_seconds() < CACHE_LIMIT: + # return this_scouting + # else: + # session.delete(this_scouting) + # session.commit() + + # def cache_scouting(pitching_card: dict, ratings_vr: dict, ratings_vl: dict) -> PitcherScouting: + # valid_bc = PitchingCardBase.model_validate(pitching_card, from_attributes=True) + # db_bc = PitchingCard.model_validate(valid_bc) + + # valid_vl = PitchingRatingsBase.model_validate(ratings_vl, from_attributes=True) + # db_vl = PitchingRatings.model_validate(valid_vl) + + # valid_vr = PitchingRatingsBase.model_validate(ratings_vr, from_attributes=True) + # db_vr = PitchingRatings.model_validate(valid_vr) + + # db_scouting = PitcherScouting( + # pitchingcard=db_bc, + # ratings_vl=db_vl, + # ratings_vr=db_vr + # ) + + # session.add(db_scouting) + # session.commit() + # session.refresh(db_scouting) + # return db_scouting + + # s_query = await db_get(f'pitchingcardratings/player/{card.player.id}', none_okay=False) + # if s_query['count'] == 2: + # scouting = cache_scouting( + # pitching_card=s_query['ratings'][0]['pitchingcard'], + # ratings_vr=s_query['ratings'][0] if s_query['ratings'][0]['vs_hand'] == 'R' else s_query['ratings'][1], + # ratings_vl=s_query['ratings'][0] if s_query['ratings'][0]['vs_hand'] == 'L' else s_query['ratings'][1] + # ) + # pos_rating = await get_position(session, card, 'P') + # return scouting + + # return None # async def get_position_rating_or_none(session: Session, card: Card, position: str, skip_cache: bool = False) -> PositionRating | None: diff --git a/in_game/managerai_responses.py b/in_game/managerai_responses.py index b2acca2..7d8755d 100644 --- a/in_game/managerai_responses.py +++ b/in_game/managerai_responses.py @@ -1,7 +1,11 @@ import pydantic -class RunResponse(pydantic.BaseModel): +class AiResponse(pydantic.BaseModel): + ai_note: str = '' + + +class RunResponse(AiResponse): min_safe: int | None = None @@ -20,7 +24,7 @@ class UncappedRunResponse(RunResponse): trail_min_safe_delta: int = 0 -class ThrowResponse(pydantic.BaseModel): +class ThrowResponse(AiResponse): cutoff: bool = False # Stops on True at_lead_runner: bool = True at_trail_runner: bool = False # Stops on False @@ -28,10 +32,20 @@ class ThrowResponse(pydantic.BaseModel): trail_max_safe_delta: int = -6 -class DefenseResponse(pydantic.BaseModel): +class DefenseResponse(AiResponse): hold_first: bool = False hold_second: bool = False hold_third: bool = False outfield_in: bool = False infield_in: bool = False corners_in: bool = False + + def defender_in(self, position: str): + if self.infield_in and position in ['C', '1B', '2B', '3B', 'SS', 'P']: + return True + elif self.corners_in and position in ['C', '1B', '3B', 'P']: + return True + elif self.outfield_in and position in ['LF', 'CF', 'RF']: + return True + return False + diff --git a/tests/command_logic/test_logic_gameplay.py b/tests/command_logic/test_logic_gameplay.py index 65f53f5..a19d954 100644 --- a/tests/command_logic/test_logic_gameplay.py +++ b/tests/command_logic/test_logic_gameplay.py @@ -1,7 +1,7 @@ import pytest from sqlmodel import Session, select, func -from command_logic.logic_gameplay import advance_runners, doubles, get_obc, get_re24, get_wpa, complete_play, log_run_scored, strikeouts, steals +from command_logic.logic_gameplay import advance_runners, doubles, gb_result_1, get_obc, get_re24, get_wpa, complete_play, log_run_scored, strikeouts, steals, xchecks from in_game.gameplay_models import Lineup, Play from tests.factory import session_fixture, Game @@ -22,7 +22,7 @@ def test_advance_runners(session: Session): assert play_2.on_second_id == play_1.batter_id play_2.pa, play_2.ab, play_2.hit, play_2.batter_final = 1, 1, 1, 1 - advance_runners(session, play_2, 1) + play_2 = advance_runners(session, play_2, 1) session.add(play_2) session.commit() @@ -35,6 +35,17 @@ def test_advance_runners(session: Session): assert play_3.on_second is None assert play_3.on_first is not None + play_3.pa, play_3.ab, play_3.hit, play_3.batter_final = 1, 1, 1, 2 + play_3 = advance_runners(session, play_3, 3, earned_bases=1) + session.add(play_3) + session.commit() + + assert play_3.rbi == 1 + assert play_2.run == 1 + assert play_2.e_run == 0 + assert play_1.run == 1 + assert play_1.e_run == 1 + def test_get_obc(): assert get_obc() == 0 @@ -207,4 +218,3 @@ async def test_stealing(session: Session): assert play_2.on_first_final == 2 assert play_2.sb == 1 assert play_2.runner == play_2.on_first - diff --git a/tests/command_logic/test_logic_groundballs.py b/tests/command_logic/test_logic_groundballs.py new file mode 100644 index 0000000..93b1cec --- /dev/null +++ b/tests/command_logic/test_logic_groundballs.py @@ -0,0 +1,348 @@ +import pytest +from sqlmodel import Session, select, func + +from command_logic.logic_gameplay import advance_runners, gb_result_10, gb_result_11, gb_result_13, gb_result_2, gb_result_3, gb_result_4, gb_result_5, gb_result_6, gb_result_7, gb_result_8, gb_result_9, singles, doubles, triples, gb_result_1, complete_play, log_run_scored, strikeouts, steals, undo_play, xchecks +from in_game.gameplay_models import Lineup, Play +from tests.factory import session_fixture, Game + + +def test_groundball_1(session: Session): + game_1 = session.get(Game, 2) + play_1 = session.get(Play, 1) + play_1.hit, play_1.batter_final = 1, 2 + play_2 = complete_play(session, play_1) + + play_2 = gb_result_1(session, play_2) + ending = complete_play(session, play_2) + + assert ending.on_second == play_2.on_second + assert play_2.batter_final == None + + +async def test_groundball_2_1(session: Session): + play_1 = session.get(Play, 2) + play_1 = await singles(session, None, play_1, '*') + play_2 = complete_play(session, play_1) + + play_2 = gb_result_2(session, play_2) + ending = complete_play(session, play_2) + + assert ending.starting_outs == 0 + assert ending.on_base_code == 0 + + +async def test_groundball_2_2(session: Session): + play_1 = session.get(Play, 2) + play_1 = await doubles(session, None, play_1, '**') + play_1.starting_outs = 0 + play_2 = complete_play(session, play_1) + + play_2 = gb_result_2(session, play_2) + ending = complete_play(session, play_2) + + assert ending.starting_outs == 2 + assert ending.on_third == play_2.on_second + + +async def test_groundball_2_3(session: Session): + play_1 = session.get(Play, 2) + play_1 = await triples(session, None, play_1) + play_1.starting_outs = 0 + play_2 = complete_play(session, play_1) + + play_2 = gb_result_2(session, play_2) + ending = complete_play(session, play_2) + + assert ending.starting_outs == 2 + assert ending.on_base_code == 0 + assert ending.away_score == 1 + + +async def test_groundball_3_1(session: Session): + play_1 = session.get(Play, 2) + play_1 = await singles(session, None, play_1, '*') + play_2 = complete_play(session, play_1) + + play_2 = gb_result_3(session, play_2) + ending = complete_play(session, play_2) + + assert ending.starting_outs == 2 + assert ending.on_base_code == 2 + assert ending.away_score == 0 + + +async def test_groundball_3_2(session: Session): + play_1 = session.get(Play, 2) + play_1 = await doubles(session, None, play_1, '**') + play_2 = complete_play(session, play_1) + + play_2 = gb_result_3(session, play_2) + ending = complete_play(session, play_2) + + assert ending.starting_outs == 2 + assert ending.on_base_code == 3 + assert ending.away_score == 0 + + +async def test_groundball_3_3(session: Session): + play_1 = session.get(Play, 2) + play_1 = await triples(session, None, play_1,) + play_2 = complete_play(session, play_1) + + play_2 = gb_result_3(session, play_2) + ending = complete_play(session, play_2) + + assert ending.starting_outs == 2 + assert ending.on_base_code == 0 + assert ending.away_score == 1 + + +async def test_groundball_4_1(session: Session): + play_1 = session.get(Play, 2) + play_1 = await singles(session, None, play_1, '*') + play_2 = complete_play(session, play_1) + + play_2 = gb_result_4(session, play_2) + ending = complete_play(session, play_2) + + assert ending.starting_outs == 2 + assert ending.on_base_code == 1 + assert ending.on_first == play_2.batter + + +async def test_groundball_4_2(session: Session): + play_1 = session.get(Play, 2) + play_1 = await singles(session, None, play_1, '*') + play_2 = complete_play(session, play_1) + play_2 = await singles(session, None, play_2, '**') + play_3 = complete_play(session, play_2) + + play_3 = gb_result_4(session, play_3) + ending = complete_play(session, play_3) + + assert ending.starting_outs == 2 + assert ending.on_base_code == 1 + assert ending.on_first == play_2.batter + assert ending.away_score == 1 + + +async def test_groundball_5_1(session: Session): + play_1 = session.get(Play, 2) + play_1 = await triples(session, None, play_1) + play_2 = complete_play(session, play_1) + + play_2 = gb_result_5(session, play_2, True) + ending = complete_play(session, play_2) + + assert ending.starting_outs == 2 + assert ending.on_base_code == 0 + assert ending.away_score == 1 + + +async def test_groundball_5_2(session: Session): + play_1 = session.get(Play, 2) + play_1 = await triples(session, None, play_1) + play_2 = complete_play(session, play_1) + + play_2 = gb_result_5(session, play_2, False) + ending = complete_play(session, play_2) + + assert ending.starting_outs == 2 + assert ending.on_base_code == 3 + assert ending.away_score == 0 + + +async def test_groundball_6_1(session: Session): + play_1 = session.get(Play, 2) + play_1 = await doubles(session, None, play_1, '**') + play_2 = complete_play(session, play_1) + + play_2 = gb_result_6(session, play_2, True) + ending = complete_play(session, play_2) + + assert ending.starting_outs == 2 + assert ending.on_base_code == 3 + + +async def test_groundball_6_2(session: Session): + play_1 = session.get(Play, 2) + play_1 = await doubles(session, None, play_1, '**') + play_2 = complete_play(session, play_1) + + play_2 = gb_result_6(session, play_2, False) + ending = complete_play(session, play_2) + + assert ending.starting_outs == 2 + assert ending.on_base_code == 2 + + +async def test_groundball_7_1(session: Session): + play_1 = session.get(Play, 2) + play_1 = await triples(session, None, play_1) + play_2 = complete_play(session, play_1) + + play_2 = gb_result_7(session, play_2) + ending = complete_play(session, play_2) + + assert ending.starting_outs == 2 + assert ending.on_base_code == 3 + assert ending.away_score == 0 + + +async def test_groundball_7_2(session: Session): + play_1 = session.get(Play, 2) + play_1 = await singles(session, None, play_1, '*') + play_2 = complete_play(session, play_1) + play_2 = await singles(session, None, play_2, '**') + play_3 = complete_play(session, play_2) + + play_3 = gb_result_7(session, play_3) + ending = complete_play(session, play_3) + + assert ending.starting_outs == 2 + assert ending.on_base_code == 6 + assert ending.away_score == 0 + + +async def test_groundball_8_1(session: Session): + play_1 = session.get(Play, 2) + play_1 = await triples(session, None, play_1) + play_2 = complete_play(session, play_1) + + play_2 = gb_result_8(session, play_2) + ending = complete_play(session, play_2) + + assert ending.starting_outs == 2 + assert ending.on_base_code == 3 + assert ending.away_score == 0 + + +async def test_groundball_8_2(session: Session): + play_1 = session.get(Play, 2) + play_1 = await singles(session, None, play_1, '*') + play_2 = complete_play(session, play_1) + play_2 = await singles(session, None, play_2, '**') + play_3 = complete_play(session, play_2) + + play_3 = gb_result_8(session, play_3) + ending = complete_play(session, play_3) + + assert ending.starting_outs == 2 + assert ending.on_base_code == 6 + assert ending.away_score == 0 + + +async def test_groundball_9_1(session: Session): + play_1 = session.get(Play, 2) + play_1 = await singles(session, None, play_1, '*') + play_2 = complete_play(session, play_1) + play_2 = await singles(session, None, play_2, '**') + play_3 = complete_play(session, play_2) + + play_3 = gb_result_9(session, play_3) + ending = complete_play(session, play_3) + + assert ending.starting_outs == 2 + assert ending.on_base_code == 6 + assert ending.away_score == 0 + + +async def test_groundball_10_1(session: Session): + play_1 = session.get(Play, 2) + play_1 = await singles(session, None, play_1, '*') + play_2 = complete_play(session, play_1) + play_2 = await singles(session, None, play_2, '*') + play_3 = complete_play(session, play_2) + play_3 = await singles(session, None, play_3, '*') + play_4 = complete_play(session, play_3) + + play_4 = gb_result_10(session, play_4) + ending = complete_play(session, play_4) + + assert ending.starting_outs == 0 + assert ending.on_base_code == 0 + assert ending.away_score == 0 + + +async def test_groundball_13_1_1(session: Session): + play_1 = session.get(Play, 2) + play_1 = await singles(session, None, play_1, '*') + play_2 = complete_play(session, play_1) + play_2 = await singles(session, None, play_2, '*') + play_3 = complete_play(session, play_2) + + play_3.check_pos == '3B' + play_3 = gb_result_13(session, play_3) + ending = complete_play(session, play_3) + + assert ending.starting_outs == 0 + assert ending.on_base_code == 0 + assert ending.away_score == 0 + + +async def test_groundball_13_1_2(session: Session): + play_1 = session.get(Play, 2) + play_1.starting_outs = 0 + play_1 = await singles(session, None, play_1, '*') + play_2 = complete_play(session, play_1) + play_2 = await singles(session, None, play_2, '*') + play_3 = complete_play(session, play_2) + + play_3.check_pos = '3B' + play_3 = gb_result_13(session, play_3) + ending = complete_play(session, play_3) + + assert ending.starting_outs == 2 + assert ending.on_base_code == 1 + assert ending.away_score == 0 + + +async def test_groundball_13_1_3(session: Session): + play_1 = session.get(Play, 2) + play_1.starting_outs = 0 + play_1 = await singles(session, None, play_1, '*') + play_2 = complete_play(session, play_1) + play_2 = await singles(session, None, play_2, '*') + play_3 = complete_play(session, play_2) + + play_3.check_pos == '1B' + play_3 = gb_result_13(session, play_3) + ending = complete_play(session, play_3) + + assert ending.starting_outs == 2 + assert ending.on_base_code == 3 + assert ending.away_score == 0 + + +async def test_groundball_13_2(session: Session): + play_1 = session.get(Play, 2) + play_1 = await singles(session, None, play_1, '*') + play_2 = complete_play(session, play_1) + play_2 = await singles(session, None, play_2, '*') + play_3 = complete_play(session, play_2) + + play_3.check_pos == 'C' + play_3 = gb_result_13(session, play_3) + ending = complete_play(session, play_3) + + assert ending.starting_outs == 0 + assert ending.on_base_code == 0 + assert ending.away_score == 0 + + +async def test_groundball_13_3(session: Session): + play_1 = session.get(Play, 2) + play_1 = await singles(session, None, play_1, '*') + play_2 = complete_play(session, play_1) + play_2 = await singles(session, None, play_2, '*') + play_3 = complete_play(session, play_2) + + play_3.check_pos == '1B' + play_3 = gb_result_13(session, play_3) + ending = complete_play(session, play_3) + + assert ending.starting_outs == 0 + assert ending.on_base_code == 0 + assert ending.away_score == 0 + + diff --git a/utilities/buttons.py b/utilities/buttons.py index 46c20c8..62c63c2 100644 --- a/utilities/buttons.py +++ b/utilities/buttons.py @@ -218,7 +218,9 @@ class ScorebugButtons(discord.ui.View): logger.info(f'User {interaction.user.id} rolling AB in Game {self.play.game.id}') this_roll = ab_roll(self.team, self.play.game, allow_chaos=not self.had_chaos) + logger.info(f'this_roll: {this_roll}') if this_roll.is_chaos: + logger.info('AB Roll Is Chaos') self.had_chaos = True else: button.disabled = True