From 736897efad158088773ee13a3a9584cb34520f28 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Mon, 4 Nov 2024 00:12:35 -0600 Subject: [PATCH] Added tag_from_second decision for AI Flyball A and B complete --- cogs/economy.py | 11 +- cogs/gameplay.py | 14 +- command_logic/logic_gameplay.py | 314 +++++++++++++++++- docker-compose.yml | 3 +- in_game/gameplay_models.py | 49 ++- in_game/gameplay_queries.py | 2 +- in_game/managerai_responses.py | 8 +- tests/gameplay_models/test_managerai_model.py | 19 ++ 8 files changed, 399 insertions(+), 21 deletions(-) diff --git a/cogs/economy.py b/cogs/economy.py index 9d7a5ca..73dee7f 100644 --- a/cogs/economy.py +++ b/cogs/economy.py @@ -317,7 +317,7 @@ class Economy(commands.Cog): value=f'{get_roster_sheet({"gsheet": current["gsheet_template"]}, allow_embed=True)}' ) embed.add_field( - name='Paper Dynasty Season 6 Guidelines', + name='Paper Dynasty Guidelines', value='https://docs.google.com/document/d/1ngsjbz8wYv7heSiPMJ21oKPa6JLStTsw6wNdLDnt-k4/edit?usp=sharing', inline=False ) @@ -326,11 +326,6 @@ class Economy(commands.Cog): value='https://docs.google.com/document/d/1wu63XSgfQE2wadiegWaaDda11QvqkN0liRurKm0vcTs/edit?usp=sharing', inline=False ) - embed.add_field( - name='Changelog', - value='https://manticorum.notion.site/Paper-Dynasty-Season-5-Updates-52e35839523d4e198808d6f503230f0a', - inline=False - ) await ctx.send(content=None, embed=embed) @pd_help_command.command(name='rewards', help='How to Earn Rewards in Paper Dynasty') @@ -685,6 +680,7 @@ class Economy(commands.Cog): for pack in p_query['packs']: p_group = None logging.debug(f'pack: {pack}') + logging.debug(f'pack cardset: {pack["pack_cardset"]}') if pack['pack_team'] is None and pack['pack_cardset'] is None: if pack['pack_type']['name'] in p_data: p_group = pack['pack_type']['name'] @@ -699,6 +695,9 @@ class Economy(commands.Cog): elif pack['pack_type']['name'] == 'MVP': p_group = f'MVP-Team-{pack["pack_team"]["id"]}-{pack["pack_team"]["sname"]}' + if pack['pack_cardset'] is not None: + p_group += f'-Cardset-{pack["pack_cardset"]["id"]}' + elif pack['pack_cardset'] is not None: if pack['pack_type']['name'] == 'Standard': p_group = f'Standard-Cardset-{pack["pack_cardset"]["id"]}-{pack["pack_cardset"]["name"]}' diff --git a/cogs/gameplay.py b/cogs/gameplay.py index b8805c1..c419fb8 100644 --- a/cogs/gameplay.py +++ b/cogs/gameplay.py @@ -8,7 +8,7 @@ from discord.ext import commands, tasks import pygsheets from api_calls import db_get -from command_logic.logic_gameplay import get_lineups_from_sheets, checks_log_interaction +from command_logic.logic_gameplay import flyballs, get_lineups_from_sheets, checks_log_interaction from exceptions import GameNotFoundException, TeamNotFoundException, PlayNotFoundException, GameException from helpers import PD_PLAYERS_ROLE_NAME, team_role, user_has_role, random_gif, random_from_list @@ -166,7 +166,7 @@ class Gameplay(commands.Cog): legal_data = await legal_check([sp_card_id], difficulty_name=league.value) if not legal_data['legal']: await interaction.edit_original_response( - content=f'It looks like this is a Ranked Legal game and {human_sp_card.player.with_desc} is not legal in {league.name} games. You can start a new game once you pick a new SP.' + content=f'It looks like this is a Ranked Legal game and {human_sp_card.player.name_with_desc} is not legal in {league.name} games. You can start a new game once you pick a new SP.' ) return @@ -193,7 +193,7 @@ class Gameplay(commands.Cog): league.value ) await interaction.edit_original_response( - content=f'The {ai_team.sname} are starting **{ai_sp_lineup.player.with_desc}**:\n\n{ai_sp_lineup.player.p_card_url}' + content=f'The {ai_team.sname} are starting **{ai_sp_lineup.player.name_with_desc}**:\n\n{ai_sp_lineup.player.p_card_url}' ) # Get AI Lineup @@ -348,7 +348,13 @@ class Gameplay(commands.Cog): with Session(engine) as session: this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='flyball') - await interaction.edit_original_response(content=f'Pow goes teh fly ball!') + this_play.locked = True + session.add(this_play) + session.commit() + + this_play = await flyballs(session, interaction, this_game, this_play, flyball_type, comp_play=False) + + await interaction.edit_original_response(content=f'Pow goes teh fly ball!\n\n{this_play}') async def setup(bot): diff --git a/command_logic/logic_gameplay.py b/command_logic/logic_gameplay.py index 6bfaf4c..c7b9205 100644 --- a/command_logic/logic_gameplay.py +++ b/command_logic/logic_gameplay.py @@ -2,11 +2,22 @@ import logging import discord from sqlmodel import Session, select +from typing import Literal from exceptions import * from in_game.game_helpers import legal_check from in_game.gameplay_models import Game, Lineup, Team, Play -from in_game.gameplay_queries import get_card_or_none, get_channel_game_or_none, get_team_or_none +from in_game.gameplay_queries import get_card_or_none, get_channel_game_or_none, get_one_lineup, get_team_or_none, get_players_last_pa +from utilities.buttons import ButtonOptions, Confirm +from utilities.embeds import image_embed +from utilities.pages import Pagination + + +def complete_play(session:Session, this_play: Play): + this_play.locked = False + this_play.complete = True + session.add(this_play) + session.commit() async def get_lineups_from_sheets(session: Session, sheets, this_game: Game, this_team: Team, lineup_num: int, roster_num: int) -> list[Lineup]: @@ -115,6 +126,307 @@ async def checks_log_interaction(session: Session, interaction: discord.Interact return this_game, owner_team, this_play + +def log_run_scored(session: Session, runner: Lineup, is_earned: bool = True): + last_ab = get_players_last_pa(session, lineup_member=runner) + last_ab.run = 1 + last_ab.e_run = 1 if is_earned else 0 + session.add(last_ab) + session.commit() + return True + + +def advance_runners(session: Session, this_play: Play, num_bases: int, is_error: bool = False, only_forced: bool = False): + this_play.rbi = 0 + + if only_forced: + if not this_play.on_first: + if this_play.on_second: + this_play.on_second_final = 2 + if this_play.on_third: + this_play.on_third_final = 3 + return + + if this_play.on_second: + if this_play.on_third: + if num_bases > 0: + this_play.on_third_final = 4 + log_run_scored(session, this_play.on_third) + this_play.rbi += 1 if not is_error else 0 + + if num_bases > 1: + this_play.on_second_final = 4 + log_run_scored(session, this_play.on_second) + this_play.rbi += 1 if not is_error else 0 + elif num_bases == 1: + this_play.on_second_final = 3 + else: + this_play.on_second_final = 2 + else: + if this_play.on_third: + this_play.on_third_final = 3 + + if num_bases > 2: + this_play.on_first_final = 4 + log_run_scored(session, this_play.on_first) + this_play.rbi += 1 if not is_error else 0 + elif num_bases == 2: + this_play.on_first_final = 3 + elif num_bases == 1: + this_play.on_first_final = 2 + else: + this_play.on_first_final = 1 + + else: + if this_play.on_third: + if num_bases > 0: + this_play.on_third_final = 4 + log_run_scored(session, this_play.on_third) + this_play.rbi += 1 if not is_error else 0 + else: + this_play.on_third_final = 3 + + if this_play.on_second: + if num_bases > 1: + this_play.on_second_final = 4 + log_run_scored(session, this_play.on_second) + this_play.rbi += 1 if not is_error else 0 + elif num_bases == 1: + this_play.on_second_final = 3 + else: + this_play.on_second_final = 2 + + if this_play.on_first: + if num_bases > 2: + this_play.on_first_final = 4 + log_run_scored(session, this_play.on_first) + this_play.rbi += 1 if not is_error else 0 + elif num_bases == 2: + this_play.on_first_final = 3 + elif num_bases == 1: + this_play.on_first_final = 2 + else: + this_play.on_first_final = 1 + + if num_bases == 4: + this_play.batter_final = 4 + this_play.rbi += 1 + this_play.run = 1 + + +async def show_outfield_cards(session: Session, interaction: discord.Interaction, this_play: Play): + lf = get_one_lineup(session, this_game=this_play.game, this_team=this_play.pitcher.team, position='LF') + cf = get_one_lineup(session, this_game=this_play.game, this_team=this_play.pitcher.team, position='CF') + rf = get_one_lineup(session, this_game=this_play.game, this_team=this_play.pitcher.team, position='RF') + this_team = this_play.pitcher.team + logging.debug(f'lf: {lf.player.name_with_desc}\n\ncf: {cf.player.name_with_desc}\n\nrf: {rf.player.name_with_desc}\n\nteam: {this_team.lname}') + + view = Pagination([interaction.user], timeout=10) + view.left_button.label = f'Left Fielder' + view.left_button.style = discord.ButtonStyle.secondary + lf_embed = image_embed( + image_url=lf.player.image, + title=f'{this_team.sname} LF', + color=this_team.color, + desc=lf.player.name, + author_name=this_team.lname, + author_icon=this_team.logo + ) + + view.cancel_button.label = f'Center Fielder' + view.cancel_button.style = discord.ButtonStyle.blurple + cf_embed = image_embed( + image_url=cf.player.image, + title=f'{this_team.sname} CF', + color=this_team.color, + desc=cf.player.name, + author_name=this_team.lname, + author_icon=this_team.logo + ) + + view.right_button.label = f'Right Fielder' + view.right_button.style = discord.ButtonStyle.secondary + rf_embed = image_embed( + image_url=rf.player.image, + title=f'{this_team.sname} RF', + color=this_team.color, + desc=rf.player.name, + author_name=this_team.lname, + author_icon=this_team.logo + ) + + page_num = 1 + embeds = [lf_embed, cf_embed, rf_embed] + + msg = await interaction.channel.send(embed=embeds[page_num], view=view) + + await view.wait() + + if view.value: + if view.value == 'left': + page_num = 0 + if view.value == 'cancel': + page_num = 1 + if view.value == 'right': + page_num = 2 + else: + await msg.edit(content=None, embed=embeds[page_num], view=None) + + view.value = None + + if page_num == 0: + view.left_button.style = discord.ButtonStyle.blurple + view.cancel_button.style = discord.ButtonStyle.secondary + view.right_button.style = discord.ButtonStyle.secondary + if page_num == 1: + view.left_button.style = discord.ButtonStyle.secondary + view.cancel_button.style = discord.ButtonStyle.blurple + view.right_button.style = discord.ButtonStyle.secondary + if page_num == 2: + view.left_button.style = discord.ButtonStyle.secondary + view.cancel_button.style = discord.ButtonStyle.secondary + view.right_button.style = discord.ButtonStyle.blurple + + view.left_button.disabled = True + view.cancel_button.disabled = True + view.right_button.disabled = True + + await msg.edit(content=None, embed=embeds[page_num], view=view) + return [lf, cf, rf][page_num] + + +async def flyballs(session: Session, interaction: discord.Interaction, this_game: Game, this_play: Play, flyball_type: Literal['a', 'ballpark', 'b', 'b?', 'c'], comp_play: bool = True) -> Play: + num_outs = 1 + + if flyball_type == 'a': + this_play.pa = 1 + this_play.ab = 1 + this_play.outs = 1 + + if this_play.starting_outs < 2: + advance_runners(session, this_play, num_bases=1) + + if this_play.on_third: + this_play.ab = 0 + + session.add(this_play) + session.commit() + + elif flyball_type == 'b' or flyball_type == 'ballpark': + this_play.pa, this_play.ab, this_play.outs = 1, 1, 1 + this_play.bpfo = 1 if flyball_type == 'ballpark' else 0 + advance_runners(session, this_play, num_bases=0) + + if this_play.starting_outs < 2 and this_play.on_third: + this_play.ab = 0 + this_play.rbi = 1 + this_play.on_third_final = 4 + log_run_scored(session, this_play.on_third) + + if this_play.starting_outs < 2 and this_play.on_second: + logging.debug(f'calling of embed') + await show_outfield_cards(session, interaction, this_play) + logging.debug(f'done with of embed') + + runner = this_play.on_second.player + view = Confirm(responders=[interaction.user], timeout=60, label_type='yes') + + if this_play.ai_is_batting: + tag_resp = this_play.managerai.tag_from_second(session, this_game) + q_text = f'{runner.name} will attempt to advance to third if the safe range is **{tag_resp.min_safe}+**, are they going?' + else: + q_text = f'Is {runner.name} attempting to tag up to third?' + + question = await interaction.channel.send( + content=q_text, + view=view + ) + await view.wait() + + if view.value: + await question.delete() + + view = ButtonOptions( + responders=[interaction.user], timeout=60, + labels=['Tagged Up', 'Hold at 2nd', 'Out at 3rd', None, None] + ) + question = await interaction.channel.send( + f'What was the result of {runner.name} tagging from second?', view=view + ) + await view.wait() + + if view.value: + await question.delete() + if view.value == 'Tagged Up': + this_play.on_second_final = 3 + elif view.value == 'Out at 3rd': + num_outs += 1 + this_play.on_second_final = None + this_play.outs = num_outs + else: + await question.delete() + else: + await question.delete() + + session.add(this_play) + session.commit() + + elif flyball_type == 'b?': + this_play.pa, this_play.ab, this_play.outs = 1, 1, 1 + + if this_play.starting_outs < 2 and this_play.on_third: + logging.debug(f'calling of embed') + await show_outfield_cards(interaction, this_play) + logging.debug(f'done with of embed') + + runner = this_play.on_second.player + view = Confirm(responders=[interaction.user], timeout=60, label_type='yes') + + if this_play.ai_is_batting: + tag_resp = this_play.managerai.tag_from_second(session, this_game) + q_text = f'{runner.name} will attempt to advance home if the safe range is **{tag_resp.min_safe}+**, are they going?' + else: + q_text = f'Is {runner.name} attempting to tag up and go home?' + + question = await interaction.channel.send( + content=q_text, + view=view + ) + await view.wait() + + if view.value: + await question.delete() + + view = Confirm(responders=[interaction.user], timeout=60, label_type='yes') + question = await interaction.channel.send( + f'Was {runner.name} thrown out?', view=view + ) + await view.wait() + + if view.value: + await question.delete() + num_outs += 1 + this_play.on_third_final = 99 + this_play.outs = num_outs + else: + await question.delete() + this_play.ab = 0 + this_play.rbi = 1 + this_play.on_third_final = 4 + log_run_scored(session, this_play.on_third) + else: + await question.delete() + + elif flyball_type == 'c': + patch_play(this_play.id, locked=True, pa=1, ab=1, outs=1) + advance_runners(this_play.id, num_bases=0) + + if comp_play: + complete_play(this_play.id) + + session.refresh(this_play) + return this_play + \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 3c9c2c5..4170e5d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: discord-app: - image: paper-dynasty-discordapp:sqlmodel-rebuild + image: manticorum67/paper-dynasty-discordapp:gameplay-rebuild restart: unless-stopped volumes: - /home/cal/Development/paper-dynasty/dev-storage:/usr/src/app/storage @@ -15,6 +15,7 @@ services: - SCOREBOARD_CHANNEL=1000521215703789609 - TZ=America/Chicago - PYTHONHASHSEED=1749583062 + - DATABASE=Dev networks: backend: diff --git a/in_game/gameplay_models.py b/in_game/gameplay_models.py index 87b7a91..664d5ee 100644 --- a/in_game/gameplay_models.py +++ b/in_game/gameplay_models.py @@ -10,7 +10,7 @@ from sqlalchemy import func, desc from api_calls import db_get, db_post from exceptions import * -from in_game.managerai_responses import JumpResponse +from in_game.managerai_responses import JumpResponse, TagResponse sqlite_url = 'sqlite:///storage/gameplay.db' @@ -182,17 +182,17 @@ class Game(SQLModel, table=True): baserunner_string = '' if curr_play.on_first is not None: - baserunner_string += f'On First: {curr_play.on_first.player.name_card_link}\n' + baserunner_string += f'On First: {curr_play.on_first.player.name_card_link('batting')}\n' if curr_play.on_second is not None: - baserunner_string += f'On Second: {curr_play.on_second.player.name_card_link}\n' + baserunner_string += f'On Second: {curr_play.on_second.player.name_card_link('batting')}\n' if curr_play.on_third is not None: - baserunner_string += f'On Third: {curr_play.on_third.player.name_card_link}' + baserunner_string += f'On Third: {curr_play.on_third.player.name_card_link('batting')}' logging.info(f'gameplay_models - Game.get_scorebug_embed - baserunner_string: {baserunner_string}') if len(baserunner_string) > 0: embed.add_field(name=' ', value=' ', inline=False) embed.add_field(name='Baserunners', value=baserunner_string) - embed.add_field(name='Catcher', value=curr_play.catcher.player.name_card_link) + embed.add_field(name='Catcher', value=curr_play.catcher.player.name_card_link('batter')) ai_note = curr_play.ai_note logging.info(f'gameplay_models - Game.get_scorebug_embed - ai_note: {ai_note}') @@ -254,6 +254,10 @@ class Game(SQLModel, table=True): e_msg = f'Could not set the initial pitcher, catcher, and batter' log_exception(LineupsMissingException, e_msg) + manager_ai_id = ((datetime.datetime.now().day * (self.away_team_id if self.ai_team == 'away' else self.home_team_id)) % 3) + 1 + if manager_ai_id > 3 or manager_ai_id < 1: + manager_ai_id = 1 + new_play = Play( game=self, play_num=1, @@ -262,7 +266,8 @@ class Game(SQLModel, table=True): batter_pos=leadoff_batter.position, catcher=home_catcher, is_tied=True, - is_new_inning=True + is_new_inning=True, + managerai_id=manager_ai_id ) session.add(new_play) session.commit() @@ -286,7 +291,7 @@ class ManagerAi(ManagerAiBase, table=True): plays: list['Play'] = Relationship(back_populates='managerai') def create_ai(session: Session = None): def get_new_ai(this_session: Session): - all_ai = session.exec(select(ManagerAi.id)).all() + all_ai = this_session.exec(select(ManagerAi.id)).all() if len(all_ai) == 0: logging.info(f'Creating ManagerAI records') new_ai = [ @@ -388,6 +393,30 @@ class ManagerAi(ManagerAiBase, table=True): this_resp.min_safe = 6 return this_resp + + def tag_from_second(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 KeyError(f'No game found while checking tag_from_second') + + 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 = 4 + elif adjusted_running >= 5: + this_resp.min_safe = 7 + else: + this_resp.min_safe = 10 + + if this_play.starting_outs == 1: + this_resp.min_safe -= 2 + else: + this_resp.min_safe += 2 + + return this_resp class CardsetBase(SQLModel): @@ -705,6 +734,12 @@ class Play(PlayBase, table=True): else: return self.batting_ai_note + @property + def ai_is_batting(self) -> bool: + if (self.game.ai_team.lower() == 'away' and self.inning_half.lower() == 'top') or (self.game.ai_team.lower() == 'home' and self.inning_half.lower() == 'bot'): + return True + else: + return False """ BEGIN DEVELOPMENT HELPERS diff --git a/in_game/gameplay_queries.py b/in_game/gameplay_queries.py index e4e9fc8..8467efd 100644 --- a/in_game/gameplay_queries.py +++ b/in_game/gameplay_queries.py @@ -247,7 +247,7 @@ def get_game_lineups(session: Session, this_game: Game, specific_team: Team = No def get_players_last_pa(session: Session, lineup_member: Lineup, none_okay: bool = False): - last_pa = session.exec(select(Play).where(Play.game == lineup_member.game, Play.batter == lineup_member).order_by(Play.id.desc().limit(1))).all() + last_pa = session.exec(select(Play).where(Play.game == lineup_member.game, Play.batter == lineup_member).order_by(Play.id.desc()).limit(1)).all() if len(last_pa) == 1: return last_pa[0] else: diff --git a/in_game/managerai_responses.py b/in_game/managerai_responses.py index f1a19d3..5b7f2d0 100644 --- a/in_game/managerai_responses.py +++ b/in_game/managerai_responses.py @@ -1,6 +1,12 @@ import pydantic + class JumpResponse(pydantic.BaseModel): min_safe: int | None = None must_auto_jump: bool = False - run_if_auto_jump: bool = False \ No newline at end of file + run_if_auto_jump: bool = False + + +class TagResponse(pydantic.BaseModel): + min_safe: int | None = None + diff --git a/tests/gameplay_models/test_managerai_model.py b/tests/gameplay_models/test_managerai_model.py index 1192444..5bf84e2 100644 --- a/tests/gameplay_models/test_managerai_model.py +++ b/tests/gameplay_models/test_managerai_model.py @@ -34,4 +34,23 @@ def test_check_jump(session: Session): assert balanced_ai.check_jump(session, this_game, to_base=4) == JumpResponse(min_safe=None) assert aggressive_ai.check_jump(session, this_game, to_base=4) == JumpResponse(min_safe=5) + + +def test_tag_from_second(session: Session): + balanced_ai = session.exec(select(ManagerAi).where(ManagerAi.name == 'Balanced')).one() + aggressive_ai = session.exec(select(ManagerAi).where(ManagerAi.name == 'Yolo')).one() + + this_game = session.get(Game, 1) + runner = session.get(Lineup, 5) + this_play = session.get(Play, 2) + this_play.on_second = runner + + assert this_play.starting_outs == 1 + + balanced_resp = balanced_ai.tag_from_second(session, this_game) + aggressive_resp = aggressive_ai.tag_from_second(session, this_game) + + assert balanced_resp.min_safe == 5 + assert aggressive_resp.min_safe == 2 + \ No newline at end of file