diff --git a/cogs/gameplay.py b/cogs/gameplay.py index d42c88d..6abeb4a 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, frame_checks, 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 command_logic.logic_gameplay import advance_runners, bunts, chaos, complete_game, doubles, flyballs, frame_checks, 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, lineouts, 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 @@ -433,7 +433,7 @@ class Gameplay(commands.Cog): session, interaction, this_play, - buffer_message='Double logged' if this_play.starting_outs + this_play.outs < 3 and ((this_play.on_second and flyball_type == 'b') or (this_play.on_third and flyball_type == '?b')) else None + buffer_message='Double logged' if this_play.starting_outs + this_play.outs < 3 and ((this_play.on_second and flyball_type == 'b') or (this_play.on_third and flyball_type == 'b?')) else None ) @group_log.command(name='frame-pitch', description=f'Walk/strikeout split; determined by home plate umpire') @@ -451,6 +451,16 @@ class Gameplay(commands.Cog): buffer_message='Frame check logged' ) + @group_log.command(name='lineout', description='Lineouts: one out, ballpark, max outs') + async def log_lineout(self, interaction: discord.Interaction, lineout_type: Literal['one-out', 'ballpark', 'max-outs']): + with Session(engine) as session: + this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log lineout') + + logger.info(f'log lineout - this_play: {this_play}') + this_play = await lineouts(session, interaction, this_play, lineout_type) + + await self.complete_and_post_play(session, interaction, this_play, buffer_message='Lineout logged' if this_play.on_base_code > 3 else None) + @group_log.command(name='single', description='Singles: *, **, ballpark, uncapped') async def log_single( self, interaction: discord.Interaction, single_type: Literal['*', '**', 'ballpark', 'uncapped']): diff --git a/command_logic/logic_gameplay.py b/command_logic/logic_gameplay.py index 77e2142..84b8193 100644 --- a/command_logic/logic_gameplay.py +++ b/command_logic/logic_gameplay.py @@ -10,7 +10,7 @@ from sqlalchemy import delete from typing import Literal from api_calls import db_delete, db_get, db_post -from dice import frame_plate_check, sa_fielding_roll +from dice import d_twenty_roll, frame_plate_check, sa_fielding_roll from exceptions import * from helpers import DEFENSE_LITERAL, SBA_COLOR, get_channel from in_game.game_helpers import legal_check @@ -870,94 +870,254 @@ async def flyballs(session: Session, interaction: discord.Interaction, 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) + this_of = await show_outfield_cards(session, interaction, this_play) + of_rating = await get_position(session, this_of.card, this_of.position) + of_mod = 0 + if this_of.position == 'LF': + of_mod = -2 + elif this_of.position == 'RF': + of_mod = 2 logger.debug(f'done with of embed') - runner = this_play.on_second.player - view = Confirm(responders=[interaction.user], timeout=60, label_type='yes') + runner_lineup = this_play.on_second + runner = runner_lineup.player + + max_safe = runner_lineup.card.batterscouting.battingcard.running + of_rating.arm + of_mod + min_out = 20 + of_rating.arm + of_mod + if (min_out >= 20 and max_safe >= 20) or min_out > 20: + min_out = 21 + min_hold = max_safe + 1 + + safe_string = f'1{" - " if max_safe > 1 else ""}' + if max_safe > 1: + if max_safe <= 20: + safe_string += f'{max_safe}' + else: + safe_string += f'20' + + if min_out > 20: + out_string = 'None' + else: + out_string = f'{min_out}{" - 20" if min_out < 20 else ""}' + + hold_string = '' + if max_safe != min_out: + hold_string += f'{min_hold}' + if min_out - 1 > min_hold: + hold_string += f' - {min_out - 1}' + + ranges_embed = this_of.team.embed + ranges_embed.title = f'Tag Play' + ranges_embed.description = f'{this_of.team.abbrev} {this_of.position} {this_of.card.player.name}\'s Throw vs {runner.name}' + ranges_embed.add_field(name=f'{this_of.position} Arm', value=f'{"+" if of_rating.arm > 0 else ""}{of_rating.arm}') + ranges_embed.add_field(name=f'Runner Speed', value=runner_lineup.card.batterscouting.battingcard.running) + ranges_embed.add_field(name=f'{this_of.position} Mod', value=f'{of_mod}', inline=False) + ranges_embed.add_field(name='Safe Range', value=safe_string) + ranges_embed.add_field(name='Hold Range', value=hold_string) + ranges_embed.add_field(name='Out Range', value=out_string) + await interaction.channel.send( + content=None, + embed=ranges_embed + ) if this_play.ai_is_batting: tag_resp = this_play.managerai.tag_from_second(session, this_game) - 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 + logger.info(f'tag_resp: {tag_resp}') + tagging_from_second = tag_resp.min_safe >= max_safe + if tagging_from_second: + await interaction.channel.send( + content=f'**{runner.name}** is tagging from second!' + ) else: - await question.delete() + await interaction.channel.send( + content=f'**{runner.name}** is holding at second.' + ) else: - await question.delete() + tagging_from_second = await ask_confirm( + interaction, + question=f'Is {runner.name} attempting to tag up from second?', + label_type='yes', + ) + + if tagging_from_second: + this_roll = d_twenty_roll(this_play.pitcher.team, this_play.game) + if min_out is not None and this_roll.d_twenty >= min_out: + result = 'out' + elif this_roll.d_twenty <= max_safe: + result = 'safe' + else: + result = 'holds' + + await interaction.channel.send(content=None, embeds=this_roll.embeds) + + is_correct = await ask_confirm( + interaction, + question=f'Looks like {runner.name} {"is" if result != 'holds' else ""} **{result.upper()}** at {"third" if result != 'holds' else "second"}! Is that correct?', + label_type='yes', + delete_question=False + ) + + if not is_correct: + view = ButtonOptions( + responders=[interaction.user], timeout=60, + labels=['Safe at 3rd', 'Hold at 2nd', 'Out at 3rd', None, None] + ) + question = await interaction.channel.send( + f'What was the result of {runner.name} tagging from second?', view=view + ) + await view.wait() + + if view.value: + await question.delete() + if view.value == 'Tagged Up': + result = 'safe' + elif view.value == 'Out at 3rd': + result = 'out' + else: + result = 'holds' + else: + await question.delete() + + if result == 'safe': + this_play.on_second_final = 3 + elif result == 'out': + num_outs += 1 + this_play.on_second_final = None + this_play.outs = num_outs elif flyball_type == 'b?': this_play.pa, this_play.ab, this_play.outs = 1, 1, 1 + this_play = advance_runners(session, this_play, 0) if this_play.starting_outs < 2 and this_play.on_third: logger.debug(f'calling of embed') - await show_outfield_cards(session, interaction, this_play) + this_of = await show_outfield_cards(session, interaction, this_play) + of_rating = await get_position(session, this_of.card, this_of.position) logger.debug(f'done with of embed') - runner = this_play.on_second.player - view = Confirm(responders=[interaction.user], timeout=60, label_type='yes') + runner_lineup = this_play.on_third + runner = runner_lineup.player + + max_safe = runner_lineup.card.batterscouting.battingcard.running + of_rating.arm + + safe_string = f'1{" - " if max_safe > 1 else ""}' + if max_safe > 1: + if max_safe <= 20: + safe_string += f'{max_safe - 1}' + else: + safe_string += f'20' + + if max_safe == 20: + out_string = 'None' + catcher_string = '20' + elif max_safe > 20: + out_string = 'None' + catcher_string = 'None' + elif max_safe == 19: + out_string = 'None' + catcher_string = '19 - 20' + elif max_safe == 18: + out_string = f'20' + catcher_string = '18 - 19' + else: + out_string = f'{max_safe + 2} - 20' + catcher_string = f'{max_safe} - {max_safe + 1}' + + true_max_safe = max_safe - 1 + true_min_out = max_safe + 2 + + ranges_embed = this_play.batter.team.embed + ranges_embed.title = f'Play at the Plate' + ranges_embed.description = f'{runner.name} vs {this_of.card.player.name}\'s Throw' + ranges_embed.add_field(name=f'{this_of.position} Arm', value=f'{"+" if of_rating.arm > 0 else ""}{of_rating.arm}') + ranges_embed.add_field(name=f'Runner Speed', value=runner_lineup.card.batterscouting.battingcard.running) + ranges_embed.add_field(name="", value="", inline=False) + ranges_embed.add_field(name='Safe Range', value=safe_string) + ranges_embed.add_field(name='Catcher Check', value=catcher_string) + ranges_embed.add_field(name='Out Range', value=out_string) + await interaction.channel.send( + content=None, + embed=ranges_embed + ) if this_play.ai_is_batting: - tag_resp = this_play.managerai.tag_from_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?' + tag_resp = this_play.managerai.tag_from_third(session, this_game) + logger.info(f'tag_resp: {tag_resp}') + tagging_from_third = tag_resp.min_safe <= max_safe + if tagging_from_third: + await interaction.channel.send( + content=f'**{runner.name}** is tagging from third!' + ) + else: + await interaction.channel.send( + content=f'**{runner.name}** is holding at third.' + ) else: - 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 + tagging_from_third = await ask_confirm( + interaction, + question=f'Is {runner.name} attempting to tag up from third?', + label_type='yes', ) - await view.wait() + + if tagging_from_third: + this_roll = d_twenty_roll(this_play.batter.team, this_play.game) + if this_roll.d_twenty <= true_max_safe: + result = 'safe' + q_text = f'Looks like {runner.name} is SAFE at home!' + out_at_home = False + elif this_roll.d_twenty >= true_min_out: + result = 'out' + q_text = f'Looks like {runner.name} is OUT at home!' + out_at_home = True + else: + result = 'catcher' + q_text = f'Looks like this is a check for {this_play.catcher.player.name} to block the plate!' + + await interaction.channel.send(content=None, embeds=this_roll.embeds) - if view.value: - await question.delete() + is_correct = await ask_confirm( + interaction, + question=f'{q_text} Is that correct?', + label_type='yes', + delete_question=False + ) + + if not is_correct: + out_at_home = await ask_confirm( + interaction, + question=f'Was {runner.name} thrown out?', + label_type='yes' + ) + + elif result == 'catcher': + catcher_rating = await get_position(session, this_play.catcher.card, 'C') + this_roll = d_twenty_roll(this_play.catcher.team, this_play.game) + + if catcher_rating.range == 1: + safe_range = 3 + elif catcher_rating.range == 2: + safe_range = 7 + elif catcher_rating.range == 3: + safe_range = 11 + elif catcher_rating.range == 4: + safe_range = 15 + elif catcher_rating.range == 5: + safe_range = 19 + + out_at_home = True + + if this_roll.d_twenty <= safe_range: + out_at_home = False + + if out_at_home: num_outs += 1 - this_play.on_third_final = 99 + this_play.on_third_final = None 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 @@ -970,6 +1130,100 @@ async def flyballs(session: Session, interaction: discord.Interaction, this_play return this_play +async def lineouts(session: Session, interaction: discord.Interaction, this_play: Play, lineout_type: Literal['one-out', 'ballpark', 'max-outs']) -> Play: + """ + Commits this_play + """ + num_outs = 1 + this_play.pa, this_play.ab, this_play.outs = 1, 1, 1 + this_play.bplo = 1 if lineout_type == 'ballpark' else 0 + this_play = advance_runners(session, this_play, num_bases=0) + + if lineout_type == 'max-outs' and this_play.on_base_code > 0 and this_play.starting_outs < 2: + logger.info(f'Lomax going in') + if this_play.on_base_code <= 3 or this_play.starting_outs == 1: + logger.info(f'Lead runner is out') + this_play.outs = 2 + + if this_play.on_third is not None: + this_play.on_third_final = None + + elif this_play.on_second is not None: + this_play.on_second_final = None + + elif this_play.on_first is not None: + this_play.on_first_final = None + + else: + logger.info(f'Potential triple play') + this_roll = d_twenty_roll(this_play.pitcher.team, this_play.game) + ranges_embed = this_play.pitcher.team.embed + ranges_embed.title = f'Potential Triple Play' + ranges_embed.description = f'{this_play.pitcher.team.lname}' + ranges_embed.add_field(name=f'Double Play Range', value='1 - 13') + ranges_embed.add_field(name=f'Triple Play Range', value='14 - 20') + await interaction.edit_original_response( + content=None, + embeds=[ranges_embed, *this_roll.embeds] + ) + + if this_roll.d_twenty > 13: + logger.info(f'Roll of {this_roll.d_twenty} is a triple play!') + num_outs = 3 + else: + logger.info(f'Roll of {this_roll.d_twenty} is a double play!') + num_outs = 2 + + is_correct = await ask_confirm( + interaction, + question=f'Looks like this is a {"triple" if num_outs == 3 else "double"} play! Is that correct?' + ) + + if not is_correct: + logger.warning(f'{interaction.user.name} marked this result incorrect') + num_outs = 2 if num_outs == 3 else 3 + + if num_outs == 2: + logger.info(f'Lead baserunner is out') + this_play.outs = 2 + out_marked = False + + if this_play.on_third is not None: + this_play.on_third_final = None + out_marked = True + + elif this_play.on_second and not out_marked: + this_play.on_second_final = None + out_marked = True + + elif this_play.on_first and not out_marked: + this_play.on_first_final = None + out_marked = True + + else: + logger.info(f'Two baserunners are out') + this_play.outs = 3 + outs_marked = 1 + + if this_play.on_third is not None: + this_play.on_third_final = None + outs_marked += 1 + + elif this_play.on_second is not None: + this_play.on_second_final = None + outs_marked += 1 + + elif this_play.on_first is not None and outs_marked < 3: + this_play.on_first_final = None + outs_marked += 1 + + session.add(this_play) + session.commit() + + session.refresh(this_play) + return this_play + + async def frame_checks(session: Session, interaction: discord.Interaction, this_play: Play): """ Commits this_play diff --git a/dice.py b/dice.py index 8c61719..f597bba 100644 --- a/dice.py +++ b/dice.py @@ -40,6 +40,10 @@ class FrameRoll(DiceRoll): is_walk: bool = False +class DTwentyRoll(DiceRoll): + pass + + def get_dice_embed(team: Team = None, embed_title: str = None): if team: embed = discord.Embed( @@ -2837,6 +2841,26 @@ def ab_roll(this_team: Team, this_game: Game, allow_chaos: bool = True) -> AbRol return this_roll +def d_twenty_roll(this_team: Team, this_game: Game) -> DTwentyRoll: + logger.info(f'Rolling a d20 for {this_team.sname} in Game {this_game.id}') + this_roll = DTwentyRoll( + d_twenty=random.randint(1, 20) + ) + + this_roll.roll_message = f'```md\n# {this_roll.d_twenty}\nDetails:[1d20 ({this_roll.d_twenty})]\n```' + logger.info(f'D20 roll with message: {this_roll}') + + embed = get_dice_embed(this_team) + embed.add_field( + name=f'D20 roll for the {this_team.sname}', + value=this_roll.roll_message + ) + + this_roll.embeds = [embed] + + logger.info(f'Game {this_game.id} | Team {this_team.id} ({this_team.abbrev}): {this_roll.roll_message}') + return this_roll + def jump_roll(this_team: Team, this_game: Game) -> JumpRoll: """ Check for a baserunner's jump before stealing diff --git a/in_game/gameplay_models.py b/in_game/gameplay_models.py index 9c5c628..63c0ebf 100644 --- a/in_game/gameplay_models.py +++ b/in_game/gameplay_models.py @@ -563,6 +563,32 @@ class ManagerAi(ManagerAiBase, table=True): return this_resp + def tag_from_third(self, session: Session, this_game: Game) -> TagResponse: + this_resp = TagResponse() + this_play = this_game.current_play_or_none(session) + if this_play is None: + raise GameException(f'No game found while checking tag_from_third') + + ai_rd = this_play.ai_run_diff + aggression_mod = abs(self.ahead_aggression - 5 if ai_rd > 0 else self.behind_aggression - 5) + adjusted_running = self.running + aggression_mod + + if adjusted_running >= 8: + this_resp.min_safe = 7 + elif adjusted_running >= 5: + this_resp.min_safe = 10 + else: + this_resp.min_safe = 12 + + if ai_rd in [-1, 0]: + this_resp.min_safe -= 2 + + if this_play.starting_outs == 1: + this_resp.min_safe -= 2 + + return this_resp + + def throw_at_uncapped(self, session: Session, this_game: Game) -> ThrowResponse: this_resp = ThrowResponse() this_play = this_game.current_play_or_none(session)