From 2046ebcdde0f1fe694af15ab48100e3b3415bfb3 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Thu, 11 Jul 2024 15:08:06 -0500 Subject: [PATCH] Add support for Exhibition games --- cogs/admins.py | 46 ++++++++ cogs/gameplay.py | 236 +++++++++++++++++++++++++++++++++++++++++- db_calls_gameplay.py | 33 ++++-- help_text.py | 4 +- in_game/ai_manager.py | 45 ++++++-- 5 files changed, 345 insertions(+), 19 deletions(-) diff --git a/cogs/admins.py b/cogs/admins.py index 57eeea3..0a63615 100644 --- a/cogs/admins.py +++ b/cogs/admins.py @@ -9,6 +9,7 @@ from discord import Member from discord.ext import commands, tasks from discord import app_commands import in_game +from in_game import ai_manager # date = f'{datetime.datetime.now().year}-{datetime.datetime.now().month}-{datetime.datetime.now().day}' @@ -511,6 +512,51 @@ class Admins(commands.Cog): await ctx.channel.send(f'Checking fatigue for Play #{play_id} / ' f'Pitcher {"IS" if is_fatigued else "IS NOT"} fatigued') + @commands.command(name='test-exhibition', help='Mod: Test the lineup gen for exhibition games') + @commands.is_owner() + async def test_exhibition_command( + self, ctx, which: Literal['sp', 'rp', 'lineup'], team_id: int, cardset_ids: str, backup_cardset_ids: str): + if which == 'sp': + await ctx.send(f'Fetching a SP for Team ID {team_id}...') + this_pitcher = await ai_manager.get_starting_pitcher( + {'id': team_id}, + game_id=69, + is_home=True, + league_name='exhibition' + ) + await ctx.send(f'Selected Pitcher:\n{this_pitcher}') + # elif which == 'rp': + # await ctx.send(f'Fetching an RP for Team ID {team_id}...') + + @commands.command(name='test-dropdown', help='Mod: Test the custom dropdown objects') + @commands.is_owner() + async def test_dropdown_command(self, ctx): + options = [ + discord.SelectOption(label='2024 Live', value='17'), + discord.SelectOption(label='2018 Live', value='13'), + discord.SelectOption(label='2016 Live', value='11'), + discord.SelectOption(label='2008 Live', value='12'), + discord.SelectOption(label='2007 Live', value='07'), + discord.SelectOption(label='2006 Live', value='06'), + discord.SelectOption(label='2005 Live', value='05'), + discord.SelectOption(label='2004 Live', value='04'), + discord.SelectOption(label='2003 Live', value='03'), + discord.SelectOption(label='2002 Live', value='02'), + ] + + async def my_callback(interaction: discord.Interaction, values): + await interaction.response.send_message( + f'Your selection{"s are" if len(values) > 1 else " is"}: {", ".join(values)}') + + my_dropdown = Dropdown( + option_list=options, + placeholder='Select a cardset', + callback=my_callback, + max_values=8 + ) + view = DropdownView([my_dropdown]) + await ctx.send(f'Here is your dropdown:', view=view) + async def setup(bot): await bot.add_cog(Admins(bot)) diff --git a/cogs/gameplay.py b/cogs/gameplay.py index 22f594d..c635ee5 100644 --- a/cogs/gameplay.py +++ b/cogs/gameplay.py @@ -18,7 +18,8 @@ from dice import sa_fielding_roll from helpers import SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME, random_conf_gif, SBA_SEASON, PD_SEASON, IMAGES, \ get_pos_abbrev, SBA_COLOR, get_roster_lineups, give_packs, send_to_channel, \ get_channel, team_role, get_cal_user, ButtonOptions, get_ratings_guide, \ - get_team_by_owner, player_desc, player_pcard, player_bcard, get_team_embed, Confirm, get_sheets + get_team_by_owner, player_desc, player_pcard, player_bcard, get_team_embed, Confirm, get_sheets, Dropdown, \ + SELECT_CARDSET_OPTIONS, DropdownView from in_game.ai_manager import check_pitching_sub from in_game.game_helpers import single_onestar, single_wellhit, double_twostar, double_threestar, triple, \ runner_on_first, runner_on_second, runner_on_third, gb_result_1, gb_result_2, gb_result_3, gb_result_4, \ @@ -365,6 +366,8 @@ class Gameplay(commands.Cog): gt_string = ' - Gauntlet' elif 'flashback' in game.game_type: gt_string = ' - Flashback' + elif 'exhibition' in game.game_type: + gt_string = ' - Exhibition' if game_state['error']: embed = discord.Embed( @@ -1718,6 +1721,237 @@ class Gameplay(commands.Cog): ) return + @group_new_game.command(name='exhibition', description='Start a new custom game against an AI') + @commands.has_any_role(PD_PLAYERS_ROLE_NAME) + async def new_game_exhibition_command( + self, interaction: discord.Interaction, away_team_abbrev: str, home_team_abbrev: str, sp_card_id: int, + num_innings: Literal[9, 3] = 9, + cardsets: Literal['Minor League', 'Major League', 'Hall of Fame', 'Flashback', 'Custom'] = 'Custom'): + await interaction.response.defer() + + conflict = get_one_game(channel_id=interaction.channel.id, active=True) + if conflict: + await interaction.edit_original_response( + content=f'Ope. There is already a game going on in this channel. Please wait for it to complete ' + f'before starting a new one.') + return + + try: + if interaction.channel.category.name != 'Public Fields': + await interaction.response.send_message( + f'Why don\'t you head down to one of the Public Fields that way other humans can help if anything ' + f'pops up?' + ) + return + except Exception as e: + logging.error(f'Could not check channel category: {e}') + + away_team = await get_team_by_abbrev(away_team_abbrev) + home_team = await get_team_by_abbrev(home_team_abbrev) + + if not away_team: + await interaction.edit_original_response( + content=f'Sorry, I don\'t know who **{away_team_abbrev.upper()}** is.' + ) + return + if not home_team: + await interaction.edit_original_response( + content=f'Sorry, I don\'t know who **{home_team_abbrev.upper()}** is.' + ) + return + + for x in [away_team, home_team]: + if not x['is_ai']: + conflict = count_team_games(x['id']) + if conflict['count']: + await interaction.edit_original_response( + content=f'Ope. The {x["sname"]} are already playing over in ' + f'{interaction.guild.get_channel(conflict["games"][0]["channel_id"]).mention}' + ) + return + + current = await db_get('current') + week_num = current['week'] + # logging.debug(f'away: {away_team} / home: {home_team} / week: {week_num} / ranked: {is_ranked}') + logging.debug(f'away: {away_team} / home: {home_team} / week: {week_num}') + + if not away_team['is_ai'] and not home_team['is_ai']: + logging.error(f'Exhibition game between {away_team["abbrev"]} and {home_team["abbrev"]} has no AI') + await interaction.edit_original_response( + content=f'I don\'t see an AI team in this Exhibition game. Run `/new-game mlb-campaign` again with ' + f'an AI for a campaign game or `/new-game ` for a human game.' + ) + return + + ai_team = away_team if away_team['is_ai'] else home_team + human_team = away_team if home_team['is_ai'] else home_team + + if interaction.user.id not in [away_team['gmid'], home_team['gmid']]: + await interaction.edit_original_response( + content='You can only start a new game if you GM one of the teams.' + ) + return + + league_name = 'exhibition' + + this_game = post_game({ + 'away_team_id': away_team['id'], + 'home_team_id': home_team['id'], + 'week_num': week_num, + 'channel_id': interaction.channel.id, + 'active': True, + 'is_pd': True, + 'ranked': False, + 'season': current['season'], + 'short_game': True if num_innings == 3 else False, + 'game_type': league_name + }) + logging.info( + f'Game {this_game.id} ({league_name}) between {away_team_abbrev.upper()} and ' + f'{home_team_abbrev.upper()} is posted!' + ) + away_role = await team_role(interaction, away_team) + home_role = await team_role(interaction, home_team) + all_lineups = [] # Get Human SP + + human_sp_card = await db_get(f'cards', object_id=sp_card_id) + + if human_sp_card['team']['id'] != human_team['id']: + logging.error( + f'Card_id {sp_card_id} does not belong to {human_team["abbrev"]} in Game {this_game.id}' + ) + patch_game(this_game.id, active=False) + await interaction.channel.send( + f'Uh oh. Card ID {sp_card_id} is {human_sp_card["player"]["p_name"]} and belongs to ' + f'{human_sp_card["team"]["sname"]}. Will you double check that before we get started?') + return + + all_lineups.append({ + 'game_id': this_game.id, + 'team_id': human_team['id'], + 'player_id': human_sp_card['player']['player_id'], + 'card_id': sp_card_id, + 'position': 'P', + 'batting_order': 10, + 'after_play': 0 + }) + + async def get_ai_sp_roster(interaction, this_game, ai_team, home_team, league_name, all_lineups): + # Get AI Starting Pitcher + try: + await interaction.edit_original_response( + content=f'Now to decide on a Starting Pitcher...' + ) + if ai_team['id'] == this_game.away_team_id: + patch_game(this_game.id, away_roster_num=69, ai_team='away') + else: + patch_game(this_game.id, home_roster_num=69, ai_team='home') + + starter = await ai_manager.get_starting_pitcher( + ai_team, + this_game.id, + True if home_team['is_ai'] else False, + league_name + ) + all_lineups.append(starter) + ai_sp = await db_get('players', object_id=starter['player_id']) + + this_card = await db_get(f'cards', object_id=starter['card_id']) + await interaction.channel.send( + content=f'The {ai_team["sname"]} are starting **{player_desc(this_card["player"])}**:\n\n' + f'{player_pcard(this_card["player"])}' + ) + + except Exception as e: + patch_game(this_game.id, active=False) + logging.error(f'could not start an AI game with {ai_team["sname"]}: {e}') + await interaction.edit_original_response( + content=f'Looks like the {ai_team["sname"]} rotation didn\'t come through clearly. I\'ll sort ' + f'this out with {ai_team["gmname"]} and {get_cal_user(interaction).mention}. I\'ll end ' + f'this game - why don\'t you play against somebody else for now?' + ) + raise KeyError(f'A Starting Pitcher could not be found for the {ai_team["lname"]}.') + + # Get AI Lineup + try: + await interaction.edit_original_response( + content=f'I am getting a lineup card from the {ai_team["sname"]}...' + ) + + logging.info(f'new-game - calling lineup for {ai_team["abbrev"]}') + batters = await ai_manager.build_lineup( + ai_team, this_game.id, league_name, sp_name=ai_sp['p_name'] + ) + all_lineups.extend(batters) + logging.info(f'new-game - got lineup for {ai_team["abbrev"]}') + + except Exception as e: + patch_game(this_game.id, active=False) + logging.error(f'could not start an AI game with {ai_team["sname"]}: {e}') + await interaction.edit_original_response( + content=f'Looks like the {ai_team["sname"]} lineup card didn\'t come through clearly. I\'ll sort ' + f'this out with {ai_team["gmname"]} and {get_cal_user(interaction).mention}. I\'ll end ' + f'this game - why don\'t you play against somebody else for now?' + ) + return + + logging.debug(f'Setting lineup for {ai_team["sname"]} in PD game') + logging.debug(f'lineups: {all_lineups}') + post_lineups(all_lineups) + + if cardsets in ['Minor League', 'Major League', 'Hall of Fame', 'Flashback']: + if cardsets == 'Minor League': + cardset_ids = '17,8' + backup_cardset_ids = '13' + elif cardsets == 'Major League': + cardset_ids = '17,18,13,11,7,8' + backup_cardset_ids = '9,3' + elif cardsets == 'Hall of Fame': + all_c = [str(x) for x in range(1, 20)] + cardset_ids = f'{",".join(all_c)}' + backup_cardset_ids = None + else: + # Flashback cardsets + cardset_ids = '11,7,6,12' + backup_cardset_ids = '13,5' + this_game = patch_game(this_game.id, cardset_ids=cardset_ids, backup_cardset_ids=backup_cardset_ids) + + await get_ai_sp_roster(interaction, this_game, ai_team, home_team, league_name, all_lineups) + + await interaction.channel.send( + content=f'{away_role.mention} @ {home_role.mention} is set!\n\n' + f'Go ahead and set lineups with the `/read-lineup` command!', + embed=await self.initialize_play_plus_embed(this_game, full_length=False) + ) + else: + async def my_callback(interaction: discord.Interaction, values): + cardset_ids = ','.join(values) + patch_game(this_game.id, cardset_ids=cardset_ids) + # await interaction.response.send_message( + # f'Your selection{"s are" if len(values) > 1 else " is"}: {", ".join(values)}') + + await get_ai_sp_roster(interaction, this_game, ai_team, home_team, league_name, all_lineups) + + await interaction.channel.send( + content=f'{away_role.mention} @ {home_role.mention} is set!\n\n' + f'Go ahead and set lineups with the `/read-lineup` command!', + embed=await self.initialize_play_plus_embed(this_game, full_length=False) + ) + + my_dropdown = Dropdown( + option_list=SELECT_CARDSET_OPTIONS, + placeholder='Select up to 8 cardsets to include', + callback=my_callback, + max_values=len(SELECT_CARDSET_OPTIONS) + ) + view = DropdownView([my_dropdown]) + await interaction.edit_original_response( + content=None, + view=view + ) + + return + @commands.command(name='force-endgame', help='Mod: Force a game to end without stats') @commands.is_owner() async def force_end_game_command(self, ctx: commands.Context): diff --git a/db_calls_gameplay.py b/db_calls_gameplay.py index 409364c..bde6562 100644 --- a/db_calls_gameplay.py +++ b/db_calls_gameplay.py @@ -13,7 +13,7 @@ from dataclasses import dataclass from helpers import SBA_SEASON, PD_SEASON, get_player_url, get_sheets from db_calls import db_get -from in_game.data_cache import get_pd_player, CardPosition, BattingCard +from in_game.data_cache import get_pd_player, CardPosition, BattingCard, get_pd_team db = SqliteDatabase( 'storage/gameplay.db', @@ -183,6 +183,8 @@ class Game(BaseModel): first_message = IntegerField(null=True) ai_team = CharField(null=True) game_type = CharField(default='minor-league') + cardset_ids = CharField(null=True) + backup_cardset_ids = CharField(null=True) # TODO: add get_away_team and get_home_team that deals with SBa/PD and returns Team object @@ -205,6 +207,8 @@ class StratGame: first_message: int = None ai_team: str = None game_type: str = None + cardset_ids: str = None + backup_cardset_ids: str = None db.create_tables([Game]) @@ -244,7 +248,8 @@ def post_game(game_dict: dict): return_game = StratGame( new_game.id, new_game.away_team_id, new_game.home_team_id, new_game.channel_id, new_game.season, new_game.active, new_game.is_pd, new_game.ranked, new_game.short_game, new_game.week_num, new_game.game_num, - new_game.away_roster_num, new_game.home_roster_num, new_game.first_message, new_game.ai_team, new_game.game_type + new_game.away_roster_num, new_game.home_roster_num, new_game.first_message, new_game.ai_team, + new_game.game_type, new_game.cardset_ids, new_game.backup_cardset_ids ) db.close() @@ -282,18 +287,21 @@ def get_one_game(game_id=None, away_team_id=None, home_team_id=None, week_num=No return None -async def get_game_team(game: StratGame, gm_id: int = None, team_abbrev: str = None, team_id: int = None) -> dict: +async def get_game_team( + game: StratGame, gm_id: int = None, team_abbrev: str = None, team_id: int = None, + skip_cache: bool = False) -> dict: if not gm_id and not team_abbrev and not team_id: raise KeyError(f'get_game_team requires either one of gm_id, team_abbrev, or team_id to not be None') logging.debug(f'getting game team for game {game.id} / gm_id: {gm_id} / ' f'tm_abbrev: {team_abbrev} / team_id: {team_id} / game: {game}') if game.is_pd: - if gm_id: - t_query = await db_get('teams', params=[('season', PD_SEASON), ('gm_id', gm_id)]) - return t_query['teams'][0] - elif team_id: - return await db_get('teams', object_id=team_id) + if team_id: + return await get_pd_team(team_id, skip_cache=skip_cache) + elif gm_id: + return await get_pd_team(gm_id, skip_cache=skip_cache) + # t_query = await db_get('teams', params=[('season', PD_SEASON), ('gm_id', gm_id)]) + # return t_query['teams'][0] else: t_query = await db_get('teams', params=[('season', PD_SEASON), ('abbrev', team_abbrev)]) return t_query['teams'][0] @@ -308,7 +316,8 @@ async def get_game_team(game: StratGame, gm_id: int = None, team_abbrev: str = N def patch_game( game_id, away_team_id=None, home_team_id=None, week_num=None, game_num=None, channel_id=None, active=None, - first_message=None, home_roster_num=None, away_roster_num=None, ai_team=None): + first_message=None, home_roster_num=None, away_roster_num=None, ai_team=None, cardset_ids=None, + backup_cardset_ids=None): this_game = Game.get_by_id(game_id) if away_team_id is not None: this_game.away_team_id = away_team_id @@ -330,13 +339,17 @@ def patch_game( this_game.away_roster_num = away_roster_num if ai_team is not None: this_game.ai_team = ai_team + if cardset_ids is not None: + this_game.cardset_ids = cardset_ids + if backup_cardset_ids is not None: + this_game.backup_cardset_ids = backup_cardset_ids this_game.save() # return_game = model_to_dict(this_game) return_game = StratGame( this_game.id, this_game.away_team_id, this_game.home_team_id, this_game.channel_id, this_game.season, this_game.active, this_game.is_pd, this_game.ranked, this_game.short_game, this_game.week_num, this_game.game_num, this_game.away_roster_num, this_game.home_roster_num, this_game.first_message, - this_game.ai_team + this_game.ai_team, this_game.game_type, this_game.cardset_ids, this_game.backup_cardset_ids ) db.close() return return_game diff --git a/help_text.py b/help_text.py index 3252386..8d820a6 100644 --- a/help_text.py +++ b/help_text.py @@ -24,11 +24,13 @@ HELP_SHEET_SCRIPTS = ( ) HELP_GAMEMODES = ( + f'- Campaigns: Beat all 30 MLB teams to advance from the Minor League to Major League to Hall of Fame!' f'- Ranked Play: Play against another PD manager with your ranked roster.\n' f'- Unlimited Play: Play an unranked game against another PD manager. Great for casual play, playtesting rosters, ' f'and event games.\n' f'- Gauntlets: Draft a team of 26 and attempt to win 10 games before losing 2. Rewards escalate based on the ' - f'number of wins.' + f'number of wins.\n' + f'- Exhibition: Play a custom game against the AI' ) HELP_NEWGAME = ( diff --git a/in_game/ai_manager.py b/in_game/ai_manager.py index d12c8b5..bafa2a7 100644 --- a/in_game/ai_manager.py +++ b/in_game/ai_manager.py @@ -5,7 +5,8 @@ import random # import data_cache from db_calls_gameplay import StratPlay, StratGame, get_one_lineup, get_manager, get_team_lineups, \ - get_last_inning_end_play, make_sub, get_player, StratLineup, get_pitching_stats, patch_play, patch_lineup + get_last_inning_end_play, make_sub, get_player, StratLineup, get_pitching_stats, patch_play, patch_lineup, \ + get_one_game from db_calls import db_get, db_post from peewee import * from typing import Optional, Literal @@ -102,6 +103,18 @@ def batter_grading(vs_hand, rg_data): } +def get_cardset_string(this_game: StratGame): + cardsets = '' + bcardsets = '' + for x in this_game.cardset_ids.split(','): + cardsets += f'&cardset_id={x}' + if this_game.backup_cardset_ids is not None: + for x in this_game.backup_cardset_ids.split(','): + bcardsets += f'&backup_cardset_id={x}' + + return f'{cardsets}{bcardsets}' + + async def get_or_create_card(player: dict, team: dict) -> int: # get player card; create one if none found z = 0 @@ -320,8 +333,10 @@ async def build_lineup(team_object: dict, game_id: int, league_name: str, sp_nam # sorted_players = sorted(players.items(), key=lambda x: x[1]['cost'], reverse=True) build_type = 'fun' + this_game = get_one_game(game_id=game_id) l_query = await db_get( - f'teams/{team_object["id"]}/lineup/{league_name}?pitcher_name={sp_name}&build_type={build_type}', + f'teams/{team_object["id"]}/lineup/{league_name}?pitcher_name={sp_name}&build_type={build_type}' + f'{get_cardset_string(this_game)}', timeout=6 ) sorted_players = l_query['array'] @@ -358,7 +373,8 @@ async def build_lineup(team_object: dict, game_id: int, league_name: str, sp_nam return lineups -async def get_starting_pitcher(team_object: dict, game_id: int, is_home: bool, league_name: str = None) -> dict: +async def get_starting_pitcher( + team_object: dict, game_id: int, is_home: bool, league_name: str = None) -> dict: # set_params = [('cardset_id_exclude', 2)] # if league_name == 'minor-league': # set_params = copy.deepcopy(MINOR_CARDSET_PARAMS) @@ -431,7 +447,19 @@ async def get_starting_pitcher(team_object: dict, game_id: int, is_home: bool, l sp_rank = 4 else: sp_rank = 5 - starter = await db_get(f'teams/{team_object["id"]}/sp/{league_name}?sp_rank={sp_rank}') + + # acardsets = [f'&cardset_id={x}' for x in cardset_ids] if cardset_ids is not None else '' + # cardsets = '' + # bcardsets = '' + # for x in cardset_ids: + # cardsets += f'&cardset_id={x}' + # abcardsets = [f'&cardset_id={x}' for x in backup_cardset_ids] if backup_cardset_ids is not None else '' + # for x in backup_cardset_ids: + # bcardsets += f'&backup_cardset_id={x}' + this_game = get_one_game(game_id=game_id) + starter = await db_get( + f'teams/{team_object["id"]}/sp/{league_name}?sp_rank={sp_rank}{get_cardset_string(this_game)}' + ) # get player card; create one if none found card_id = await get_or_create_card(starter, team_object) @@ -475,7 +503,7 @@ async def get_relief_pitcher(this_play: StratPlay, ai_team: dict, league_name: s logging.debug(f'need: {need}') rp_query = await db_get(f'teams/{ai_team["id"]}/rp/{league_name.split("-run")[0]}' - f'?need={need}&used_pitcher_ids={id_string}') + f'?need={need}&used_pitcher_ids={id_string}{get_cardset_string(this_game)}') card_id = await get_or_create_card(rp_query, ai_team) return { 'game_id': this_play.game.id, @@ -706,13 +734,16 @@ async def check_pitching_sub(this_play: StratPlay, ai_team: dict): this_pc = await data_cache.get_pd_pitchingcard(this_play.pitcher.player_id, variant=this_play.pitcher.variant) pof_weakness = this_pc.card.starter_rating if len(used_pitchers) == 1 else this_pc.card.relief_rating innof_work = math.ceil((ps['pl_outs'] + 1) / 3) + is_starter = True if len(used_pitchers) == 1 else False gtr = this_ai.go_to_reliever( this_play, tot_allowed=ps['pl_hit'] + ps['pl_bb'] + ps['pl_hbp'], - is_starter=True if len(used_pitchers) == 1 else False + is_starter=is_starter ) - if (this_play.game.short_game or gtr or innof_work > pof_weakness + 3) and len(used_pitchers) < 8: + if (this_play.game.short_game or gtr or + (innof_work > pof_weakness + 1 and not is_starter) or + (innof_work > pof_weakness + 3 and is_starter)) and len(used_pitchers) < 8: rp_lineup = make_sub(await get_relief_pitcher(this_play, ai_team, this_play.game.game_type)) try: rp_pitcard = await data_cache.get_pd_pitchingcard(rp_lineup.player_id, rp_lineup.variant)