diff --git a/cogs/gameplay.py b/cogs/gameplay.py index a3e0f2f..f1f5f52 100644 --- a/cogs/gameplay.py +++ b/cogs/gameplay.py @@ -438,6 +438,8 @@ class Gameplay(commands.Cog): content=f'Creating this game for {t_role.mention}:\n{this_game}' ) + # TODO: add new-game exhibition, unlimited, and ranked + @commands.command(name='force-endgame', help='Mod: Force a game to end without stats') async def force_end_game_command(self, ctx: commands.Context): with Session(engine) as session: diff --git a/command_logic/logic_gameplay.py b/command_logic/logic_gameplay.py index 1f7867c..db1b42f 100644 --- a/command_logic/logic_gameplay.py +++ b/command_logic/logic_gameplay.py @@ -15,7 +15,7 @@ from exceptions import * 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_available_batters, get_position, get_available_pitchers, get_card_or_none, get_channel_game_or_none, get_db_ready_decisions, get_db_ready_plays, get_game_lineups, get_last_team_play, get_one_lineup, get_player_id_from_dict, get_player_name_from_dict, get_player_or_none, get_sorted_lineups, get_team_or_none, get_players_last_pa, post_game_rewards +from in_game.gameplay_queries import get_available_batters, get_batter_card, 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, SelectBatterSub, SelectStartingPitcher, SelectViewDefense @@ -151,7 +151,7 @@ async def get_scorebug_embed(session: Session, this_game: Game, full_length: boo good_jump = '2-11' else: good_jump = '2-12' - steal_string = f'{"*" if batting_card.steal_auto else ""}{good_jump}/- ({batting_card.steal_high}-{batting_card.steal_low})' + steal_string = f'{"`*`" if batting_card.steal_auto else ""}{good_jump}/- ({batting_card.steal_high}-{batting_card.steal_low})' return steal_string baserunner_string = '' @@ -201,8 +201,21 @@ async def get_scorebug_embed(session: Session, this_game: Game, full_length: boo cat_string = f'{curr_play.catcher.player.name_card_link('batter')}\nArm: {catcher_rating.arm}' embed.add_field(name='Catcher', value=cat_string) - ai_note = curr_play.ai_note - logger.info(f'gameplay_models - Game.get_scorebug_embed - ai_note: {ai_note}') + if curr_play.ai_is_batting and curr_play.on_base_code > 0: + if curr_play.on_base_code in [2, 4]: + to_base = 3 + elif curr_play.on_base_code in [1, 5]: + to_base = 2 + else: + to_base = 4 + + jump_resp = curr_play.managerai.check_jump(session, this_game, to_base=to_base) + ai_note = jump_resp.ai_note + else: + def_align = curr_play.managerai.defense_alignment(session, this_game) + ai_note = def_align.ai_note + logger.info(f'gameplay_models - get_scorebug_embed - ai_note: {ai_note}') + if len(ai_note) > 0: gm_name = this_game.home_team.gmname if this_game.ai_team == 'home' else this_game.away_team.gmname embed.add_field(name=f'{gm_name} will...', value=ai_note, inline=False) @@ -1276,6 +1289,13 @@ async def check_uncapped_advance(session: Session, interaction: discord.Interact 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 + runner_bc = get_batter_card(this_lineup=lead_runner) + of_rating = await get_position(session, this_card=outfielder.card, position=outfielder.position) + defense_embed = def_team.embed + defense_embed.description = f'{outfielder.player.name}\'s Throw' + trail_bc = get_batter_card(this_lineup=trail_runner) + logger.info(f'trail runner batting card: {trail_bc}') + safe_range = None # Either there is no AI team or the AI is pitching if not this_game.ai_team or not this_play.ai_is_batting: @@ -1287,6 +1307,8 @@ async def check_uncapped_advance(session: Session, interaction: discord.Interact if is_lead_running: throw_resp = None + def_alignment = this_play.managerai.defense_alignment(session, this_play.game) + if this_game.ai_team: throw_resp = this_play.managerai.throw_at_uncapped(session, this_game) logger.info(f'throw_resp: {throw_resp}') @@ -1303,6 +1325,9 @@ async def check_uncapped_advance(session: Session, interaction: discord.Interact await asyncio.sleep(1) return this_play + else: + await interaction.channel.send(content=f'{outfielder.player.name} is throwing {TO_BASE[lead_base]}!') + else: throw_for_lead = await ask_confirm( interaction=interaction, @@ -1326,36 +1351,44 @@ async def check_uncapped_advance(session: Session, interaction: discord.Interact label_type='yes' ) + lead_bc = get_batter_card(this_lineup=lead_runner) + logger.info(f'lead runner batting card: {lead_bc}') + + lead_safe_range = lead_bc.running + of_rating.arm + 1 + if lead_runner == this_play.on_second: + lead_safe_range -= 2 if def_alignment.hold_second else 0 + elif lead_runner == this_play.on_first: + lead_safe_range -= 2 if def_alignment.hold_first else 0 + logger.info(f'lead_safe_range: {lead_safe_range}') + # 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 + trail_safe = trail_bc.running - 5 + of_rating.arm + logger.info(f'trail_safe: {trail_safe}') + 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 - ) + + if throw_resp.at_trail_runner and trail_safe <= throw_resp.trail_max_safe and trail_safe <= throw_resp.trail_max_safe_delta - lead_safe_range: + logger.info(f'defense throwing at trail runner {AT_BASE[trail_base]}') + await interaction.channel.send(f'**{outfielder.player.name}** will throw {TO_BASE[trail_base]}!') + throw_lead = False else: + logger.info(f'defense throwing at lead runner {AT_BASE[lead_base]}') await interaction.channel.send(f'**{outfielder.player.name}** will throw {TO_BASE[lead_base]}!') - ai_throw_lead = True + throw_lead = True else: + view = Confirm(responders=[interaction.user], timeout=60, label_type='yes') + view.confirm.label = 'Home Plate' if lead_base == 4 else 'Third Base' + view.cancel.label = 'Third Base' if trail_base == 3 else 'Second Base' question = await interaction.channel.send( f'Is the throw going {TO_BASE[lead_base]} or {TO_BASE[trail_base]}?', view=view ) - - if not ai_throw_lead: - await view.wait() - elif ai_throw_lead: - view.value = True + throw_lead = await view.wait() # Throw is going to lead runner - if view.value: + if throw_lead: try: await question.delete() except (discord.NotFound, UnboundLocalError): @@ -1376,8 +1409,10 @@ async def check_uncapped_advance(session: Session, interaction: discord.Interact runner_thrown_out = await ask_confirm( interaction=interaction, - question='Was **{trail_runner.player.name}** thrown out {AT_BASE[trail_base]}?', - label_type='yes' + question=f'**{trail_runner.player.name}**\'s safe range is 1 -> {trail_safe} - were they thrown out {AT_BASE[trail_base]}?', + label_type='yes', + custom_confirm_label=f'Out {AT_BASE[trail_base]}', + custom_cancel_label=f'Safe {AT_BASE[trail_base]}' ) # Trail runner is thrown out @@ -1408,8 +1443,10 @@ async def check_uncapped_advance(session: Session, interaction: discord.Interact # 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' + question=f'**{lead_runner.player.name}**\'s safe range is 1 -> {lead_safe_range} - were they thrown out {AT_BASE[lead_base]}?', + label_type='yes', + custom_confirm_label=f'Out {AT_BASE[lead_base]}', + custom_cancel_label=f'Safe {AT_BASE[lead_base]}' ) # Lead runner is thrown out @@ -1437,18 +1474,32 @@ async def check_uncapped_advance(session: Session, interaction: discord.Interact 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( + runner_held = 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?', + question=f'Was **{lead_runner.player.name}** held before the pitch?', label_type='yes' ) + safe_range = runner_bc.running + of_rating.arm - 1 + if runner_held: + safe_range -= 1 + else: + safe_range += 1 - if not is_lead_running: + if this_play.starting_outs == 2: + safe_range += 2 + + if lead_base == 3: + if outfielder.position == 'RF': + safe_range += 2 + elif outfielder.position == 'LF': + safe_range -= 2 + + if safe_range > run_resp.min_safe: 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}?', + question=f'{lead_runner.player.name} is advancing {TO_BASE[lead_base]} with a safe range of **1->{safe_range}**! Is the defense throwing?', label_type='yes' ) @@ -1467,7 +1518,7 @@ async def check_uncapped_advance(session: Session, interaction: discord.Interact # Human throw is not being cut off if run_resp.send_trail: await interaction.channel.send( - f'**{trail_runner.player.name}** is advancing {TO_BASE[trail_base]} as the trail runner!', + f'**{trail_runner.player.name}** is advancing {TO_BASE[trail_base]} as the trail runner with a safe range of 1->{trail_bc.running - 5 + of_rating.arm}!', ) is_throwing_lead = await ask_confirm( interaction=interaction, @@ -1518,6 +1569,9 @@ async def check_uncapped_advance(session: Session, interaction: discord.Interact return this_play + else: + await interaction.channel.send(content=f'**{trail_runner.player.name}** is NOT trailing to {TO_BASE[trail_base]}.') + # Ball is going to lead base, ask if safe is_lead_out = await ask_confirm( interaction=interaction, @@ -1776,7 +1830,7 @@ async def bunts(session: Session, interaction: discord.Interaction, this_play: P lead_runner_out = await ask_confirm( interaction=interaction, - question=f'{runner.player.name}\'s safe range is **1 -> {runner.card.batterscouting.battingcard.running - 4 + def_pos.range}**. Is the runner out {AT_BASE[lead_base]}?', + question=f'{runner.player.name}\'s safe range is **1->{runner.card.batterscouting.battingcard.running - 4 + def_pos.range}**. Is the runner out {AT_BASE[lead_base]}?', custom_confirm_label=f'Out {AT_BASE[lead_base]}', custom_cancel_label=f'Safe {AT_BASE[lead_base]}' ) @@ -2981,7 +3035,8 @@ 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 = 1 + this_play.on_first_final = None + this_play.batter_final = 1 return this_play @@ -3071,7 +3126,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_BASE[advance_base]} with a 1-{safe_range} safe range?', + 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 ) diff --git a/in_game/gameplay_models.py b/in_game/gameplay_models.py index 6238572..c7cdbba 100644 --- a/in_game/gameplay_models.py +++ b/in_game/gameplay_models.py @@ -298,7 +298,8 @@ class ManagerAi(ManagerAiBase, table=True): return True - def check_jump(self, session: Session, this_game: Game, to_base: Literal[2, 3, 4]) -> JumpResponse | None: + def check_jump(self, session: Session, this_game: Game, to_base: Literal[2, 3, 4]) -> JumpResponse: + logger.info(f'Checking jump to {to_base} in Game {this_game.id}') this_resp = JumpResponse() this_play = this_game.current_play_or_none(session) if this_play is None: @@ -309,7 +310,16 @@ class ManagerAi(ManagerAiBase, table=True): if this_game.ai_team == 'home': run_diff = run_diff * -1 + 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 to_base == 2: + runner = this_play.on_first + if runner is None: + log_exception(CardNotFoundException, f'Attempted to check a jump to 2nd base, but no runner found on first.') + match self.steal: case 10: this_resp.min_safe = 12 + num_outs @@ -322,14 +332,36 @@ class ManagerAi(ManagerAiBase, table=True): case self.steal if self.steal > 2 and num_outs < 2 and run_diff <= 5: this_resp.min_safe = 16 + num_outs case _: - this_resp = 17 + num_outs + this_resp.min_safe = 17 + num_outs if self.steal > 7 and num_outs < 2 and run_diff <= 5: this_resp.run_if_auto_jump = True elif self.steal < 5: this_resp.must_auto_jump = True + + runner_card = runner.card.batterscouting.battingcard + if this_resp.run_if_auto_jump and runner_card.steal_auto: + this_resp.ai_note = f'- WILL SEND **{runner.player.name}** to second!' + + elif this_resp.must_auto_jump and not runner_card.steal_auto: + this_resp.ai_note = f'' + + else: + jump_safe_range = runner_card.steal_high + battery_hold + nojump_safe_range = runner_card.steal_low + battery_hold + logger.info(f'jump_safe_range: {jump_safe_range} / nojump_safe_range: {nojump_safe_range}') + + if this_resp.min_safe <= nojump_safe_range: + this_resp.ai_note = f'- SEND **{runner.player.name}** to second!' + + elif this_resp.min_safe <= jump_safe_range: + this_resp.ai_note = f'- SEND **{runner.player.name}** to second if they get the jump' elif to_base == 3: + runner = this_play.on_second + if runner is None: + log_exception(CardNotFoundException, f'Attempted to check a jump to 3rd base, but no runner found on second.') + match self.steal: case 10: this_resp.min_safe = 12 + num_outs @@ -341,16 +373,41 @@ class ManagerAi(ManagerAiBase, table=True): if self.steal == 10 and num_outs < 2 and run_diff <= 5: this_resp.run_if_auto_jump = True elif self.steal <= 5: - this_resp.must_auto_jump = True + this_resp.must_auto_jump = True + + runner_card = runner.card.batterscouting.battingcard + if this_resp.run_if_auto_jump and runner_card.steal_auto: + this_resp.ai_note = f'- SEND **{runner.player.name}** to third!' + + elif this_resp.must_auto_jump and not runner_card.steal_auto: + pass + + else: + jump_safe_range = runner_card.steal_low + battery_hold + logger.info(f'jump_safe_range: {jump_safe_range}') + + if this_resp.min_safe <= jump_safe_range: + this_resp.ai_note = f'- SEND **{runner.player.name}** to third!' elif run_diff in [-1, 0]: + runner = this_play.on_third + if runner is None: + log_exception(CardNotFoundException, f'Attempted to check a jump to home, but no runner found on third.') + if self.steal == 10: this_resp.min_safe = 5 - elif self.steal > 5: - this_resp.min_safe = 7 elif this_play.inning_num > 7 and self.steal >= 5: - this_resp.min_safe = 6 + this_resp.min_safe = 6 + elif self.steal > 5: + this_resp.min_safe = 7 + + runner_card = runner.card.batterscouting.battingcard + jump_safe_range = runner_card.steal_low - 9 + + if this_resp.min_safe <= jump_safe_range: + this_resp.ai_note = f'- SEND **{runner.player.name}** to third!' + logger.info(f'Returning jump resp to game {this_game.id}: {this_resp}') return this_resp def tag_from_second(self, session: Session, this_game: Game) -> TagResponse: @@ -402,7 +459,6 @@ class ManagerAi(ManagerAiBase, table=True): 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) @@ -577,6 +633,63 @@ class ManagerAi(ManagerAiBase, table=True): return this_resp + # @property + # def ai_note(self) -> str: # TODO: test these three functions with specific OBCs + # if self.inning_half == 'top': + # if self.game.ai_team == 'away': + # return self.batting_ai_note + # else: + # return self.pitching_ai_note + # else: + # if self.game.ai_team == 'away': + # return self.pitching_ai_note + # else: + # return self.batting_ai_note + + # @property + # def batting_ai_note(self) -> str: + # ai_note = '' # TODO: migrate Manager AI to their own local model + + # return ai_note + + # @property + # def pitching_ai_note(self) -> str: + # def_alignment = self.defense_alignment(session) + # ai_note = '' + # # Holding Baserunners + # if self.starting_outs == 2 and self.on_base_code > 0: + # if self.on_base_code == 1: + # ai_note += f'- hold {self.on_first.player.name}\n' + # elif self.on_base_code == 2: + # ai_note += f'- hold {self.on_second.player.name}\n' + # elif self.on_base_code in [4, 5, 7]: + # ai_note += f'- hold {self.on_first.player.name} on first\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' + + # # 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' + # else: + # 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' + + # return ai_note + class CardsetBase(SQLModel): id: int | None = Field(default=None, primary_key=True) @@ -1048,62 +1161,6 @@ class Play(PlayBase, table=True): return game_string - @property - def pitching_ai_note(self) -> str: - ai_note = '' - # Holding Baserunners - if self.starting_outs == 2 and self.on_base_code > 0: - if self.on_base_code == 1: - ai_note += f'- hold {self.on_first.player.name}\n' - elif self.on_base_code == 2: - ai_note += f'- hold {self.on_second.player.name}\n' - elif self.on_base_code in [4, 5, 7]: - ai_note += f'- hold {self.on_first.player.name} on first\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' - - # 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' - else: - 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' - - return ai_note - - @property - def batting_ai_note(self) -> str: - ai_note = '' # TODO: migrate Manager AI to their own local model - - return ai_note - - @property - def ai_note(self) -> str: # TODO: test these three functions with specific OBCs - if self.inning_half == 'top': - if self.game.ai_team == 'away': - return self.batting_ai_note - else: - return self.pitching_ai_note - else: - if self.game.ai_team == 'away': - return self.pitching_ai_note - else: - return self.batting_ai_note - @property def ai_is_batting(self) -> bool: if self.game.ai_team is None: diff --git a/in_game/gameplay_queries.py b/in_game/gameplay_queries.py index fada2fc..0dc8e0a 100644 --- a/in_game/gameplay_queries.py +++ b/in_game/gameplay_queries.py @@ -854,3 +854,13 @@ def get_available_batters(session: Session, this_game: Game, this_team: Team) -> logger.info(f'batters: {batters}') return batters + + +def get_batter_card(this_card: Card = None, this_lineup: Lineup = None) -> BattingCard: + if this_card is not None: + logger.info(f'Getting batter card for {this_card.player.name}') + return this_card.batterscouting.battingcard + if this_lineup is not None: + logger.info(f'Getting batter card for {this_lineup.player.name}') + return this_lineup.card.batterscouting.battingcard + log_exception(KeyError, 'Either a Card or Lineup must be provided to get_batter_card')