diff --git a/cogs/admins.py b/cogs/admins.py new file mode 100644 index 0000000..e2d7cc0 --- /dev/null +++ b/cogs/admins.py @@ -0,0 +1,180 @@ +import copy + +from helpers import * +from db_calls import * + +import math +import pygsheets + +from discord.ext import commands, tasks +from discord import app_commands + + +class Admins(commands.Cog): + def __init__(self, bot): + self.bot = bot + + async def cog_command_error(self, ctx, error): + await ctx.send(f'{error}') + + @commands.command(name='current', help='Current db info') + @commands.is_owner() + async def current_command(self, ctx): + current = await get_current() + current_string = '' + + for x in current: + current_string += f'{x}: {current[x]}\n' + + await ctx.send(current_string) + + @commands.command(name='blast', help='Megaphone') + @commands.is_owner() + async def blast_command(self, ctx, channel_name, *, message): + await send_to_channel(self.bot, channel_name, message) + await ctx.send(random_conf_gif()) + + @app_commands.command(name='blast', description='Send a message') + @app_commands.guilds(discord.Object(id=os.environ.get('GUILD_ID'))) + @app_commands.checks.has_any_role('Da Commish') + async def blast_slash( + self, interaction: discord.Interaction, channel: discord.TextChannel, msg_content: str = None, + embed_title: str = None, embed_field_name: str = None, image_url: str = None, color_hex: str = None): + current = await get_current() + + try: + embed = None + if embed_title: + embed = discord.Embed( + title=embed_title, + color=int(color_hex, 16) if color_hex is not None else int('0xa5fffc', 16) + ) + embed.set_footer(text=f'SBa Season {current["season"]}', icon_url=LOGO) + embed.set_image(url=image_url) + if embed_field_name: + embed.add_field( + name=embed_field_name if embed_field_name is not None else "** **", + value=msg_content + ) + await channel.send(content=None, embed=embed) + await interaction.response.send_message(content=random_conf_gif()) + return + + await channel.send(content=msg_content) + except Exception as e: + logging.error(f'Error blasting a message: {type(e)}: {e}') + await interaction.response.send_message(content=f'Uh oh\n\n{type(e)}: {e}') + + @commands.command(name='sendstats', help='all, batting, pitching') + @commands.is_owner() + async def send_stats_command(self, ctx, which='all'): + trans_cog = self.bot.get_cog('Transactions') + await trans_cog.send_stats_to_sheets(which=which) + + @commands.command(name='test', hidden=True) + @commands.is_owner() + async def test_command(self, ctx): + current = await get_current() + week_num = f'Week {current["week"]}' + stars = f'{"":*<32}' + freeze_message = f'```\n' \ + f'{stars}\n'\ + f'{week_num: >9} Freeze Period Begins\n' \ + f'{stars}\n```' + + await send_to_channel(self.bot, 'general', freeze_message) + + @commands.command(name='sendmoves', help='Send moves to sheets') + @commands.is_owner() + async def send_moves_command(self, ctx): + current = await get_current() + await ctx.send('Authenticating with sheets...') + sheets = pygsheets.authorize(service_file='storage/major-domo-service-creds.json') + trans_tab = sheets.open_by_key(SBA_ROSTER_KEY).worksheet_by_title('Transactions') + await ctx.send('Collecting transactions...') + all_vals = [] + all_moves = await get_transactions( + season=current['season'], + timeout=30 + ) + await ctx.send(f'Processing transactions ({len(all_moves)} found)...') + total_moves = len(all_moves) + + counter = 0 + for move in [*all_moves.values()]: + all_vals.insert( + 0, + [ + move['player']['name'], + move['oldteam']['sname'], + move['newteam']['sname'], + move['week'], + total_moves + 416 - counter + ] + ) + counter += 1 + logging.warning(f'all_vals samples:\n0: {all_vals[0]}\n100: {all_vals[100]}\n1000: {all_vals[1000]}\n' + f'2000: {all_vals[2000]}') + + await ctx.send('Sending transactions to sheets...') + try: + trans_tab.update_values( + crange=f'A420', + values=all_vals + ) + await ctx.send('All done!') + except Exception as e: + await ctx.send('Failed sending to sheets') + + @commands.command(name='xpick', help='Expansion pick') + @commands.is_owner() + async def expansion_pick_command(self, ctx, team_abbrev, *, name): + current = await get_current() + player_cog = self.bot.get_cog('Players') + player_name = await fuzzy_player_search(ctx, ctx.channel, self.bot, name, player_cog.player_list.keys()) + player = await get_one_player(player_name) + team = await get_one_team(team_abbrev) + old_team = copy.deepcopy(player["team"]) + + if not team: + await ctx.send(f'Who the fuck is **{team_abbrev}**? Get your shit together - it\'s DRAFT TIME!!!') + return + + if old_team['id'] == 99: + await ctx.send(f'Tell that bastard they\'re an idiot. {player["name"]} is a Free Agent.') + return + + await patch_player(player['id'], team_id=team['id']) + await ctx.send(content=None, embed=await get_player_embed(await get_one_player(player['id']), current)) + await send_to_channel( + self.bot, + 's4-draft-picks', + f'Expansion Draft: {await get_emoji(ctx, team["sname"])}{team["sname"]} select **{player["name"]}** from ' + f'{await get_emoji(ctx, old_team["sname"])}{old_team["abbrev"]}' + ) + + @commands.command(name='injimport') + @commands.is_owner() + async def injury_import_command(self, ctx): + sheets = pygsheets.authorize(service_file='storage/major-domo-service-creds.json') + inj_tab = sheets.open_by_key('1uKRf7YwTcEfp8D7gUutQRwnOHW37hl6XcBbtqw3PbN4').worksheet_by_title('Sheet1') + raw_data = inj_tab.get_values('A1', 'B545') + + for line in raw_data: + player = await get_one_player(line[0]) + await patch_player(player['id'], pitcher_injury=line[1]) + + @commands.command(name='setdemweek', help='Set player\'s demotion week') + @commands.is_owner() + async def set_dem_week_command(self, ctx, week_num, *, player_name): + player_cog = self.bot.get_cog('Players') + player_name = await fuzzy_player_search(ctx, ctx.channel, self.bot, player_name, player_cog.player_list.keys()) + player = await get_one_player(player_name) + + await patch_player(player['id'], demotion_week=week_num) + await ctx.send(random_conf_gif()) + + + +async def setup(bot): + await bot.add_cog(Admins(bot)) diff --git a/cogs/dice.py b/cogs/dice.py new file mode 100644 index 0000000..0087806 --- /dev/null +++ b/cogs/dice.py @@ -0,0 +1,1419 @@ +import re + +from helpers import * +from db_calls import * +import discord +from discord.ext import commands, tasks +from discord import app_commands +from discord.app_commands import Choice +import random + + +class Dice(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.rolls = [] + self.current = None + + self.updates.start() + + async def cog_command_error(self, ctx, error): + await ctx.send(f'{error}') + + async def get_dice_embed(self, channel, title, message): + try: + team_abbrev = re.split('-', channel.name) + if len(team_abbrev[0]) <= 4 and team_abbrev not in ['the', 'city']: + team = await get_one_team(team_abbrev[0], timeout=0.5) + else: + team = None + except (ValueError, AttributeError, requests.ReadTimeout) as e: + logging.info(f'{type(e)}: {e}') + team = None + + if team: + embed = discord.Embed( + color=int(team["dice_color"], 16) if team["dice_color"] else int(team["color"], 16) + ) + else: + embed = discord.Embed( + color=int('0x000000', 16) + ) + + if title and message: + embed.add_field(name=title, value=message) + + return embed + + @tasks.loop(minutes=5) + async def updates(self): + self.current = await get_current() + if len(self.rolls) > 0: + all_rolls = self.rolls + self.rolls = [] + await post_dice(all_rolls) + + @commands.command(name='ab', aliases=['atbat', 'swing', 'pa'], help='ab, atbat, or swing') + async def ab_roll(self, ctx): + """ + Make an AB roll including potential pitcher injuries. + """ + team = None + + d_twenty = random.randint(1, 20) + d_twenty_two = random.randint(1, 20) + flag = None + + if d_twenty == 1: + flag = 'wild pitch' + elif d_twenty == 2: + if random.randint(1, 2) == 1: + flag = 'balk' + else: + 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 = await self.get_dice_embed(ctx.channel, f'Chaos roll for {ctx.author.name}', roll_message) + embed.set_footer( + text='If there are no baserunners, ignore this chaos. If the chaos roll failed, ignore future chaos ' + 'until a new batter comes to the plate.' + ) + await ctx.channel.send( + content=None, + embed=embed + ) + return + + d_six_one = random.randint(1, 6) + d_six_two = random.randint(1, 6) + d_six_three = random.randint(1, 6) + d_twenty = random.randint(1, 20) + + roll_message = f'```md\n# {d_six_one},{d_six_two + d_six_three},'\ + f'{d_twenty}\nDetails:[1d6;2d6;1d20 ({d_six_one} - {d_six_two} {d_six_three} - '\ + f'{d_twenty})]```' + + if d_six_one == 6 and d_six_two + d_six_three > 6: + roll_message += f'\n**Check injury for pitcher injury rating {13 - d_six_two - d_six_three}**\n' \ + f'Oops! All injuries!' + + logging.info(f'roll_message: {roll_message}') + await ctx.channel.send( + content=None, + embed=await self.get_dice_embed(ctx.channel, f'At bat roll for {ctx.author.name}', roll_message) + ) + this_roll = { + 'season': self.current['season'], + 'week': self.current['week'], + 'team_id': team["id"] if team else None, + 'roller': ctx.author.id, + 'dsix': d_six_one, + 'twodsix': d_six_two + d_six_three, + 'dtwenty': d_twenty + } + self.rolls.append(this_roll) + + @commands.command(name='1d20', aliases=['d20'], help='1d20 or d20') + async def dtwenty_roll(self, ctx): + """ + Roll a single d20. + """ + team = None + d_twenty = random.randint(1, 20) + + roll_message = f'```md\n# {str(d_twenty)}\n'\ + f'Details:[1d20 ({d_twenty})]```' + + await ctx.channel.send( + content=None, + embed=await self.get_dice_embed(ctx.channel, f'd20 roll for {ctx.author.name}', roll_message) + ) + this_roll = { + 'season': self.current['season'], + 'week': self.current['week'], + 'team_id': team["id"] if team else None, + 'roller': ctx.author.id, + 'dtwenty': d_twenty + } + self.rolls.append(this_roll) + + @commands.command(name='af', aliases=['fielding'], help='Advanced fielding') + async def fielding_roll(self, ctx, *args): + """ + Make an Advanced fielding check. + """ + team = None + d_six_one = random.randint(1, 6) + d_six_two = random.randint(1, 6) + d_six_three = random.randint(1, 6) + d_twenty = random.randint(1, 20) + image = None + + if args: + if args[0][:2].upper() == '1B': + image = 'https://media.discordapp.net/attachments/643871298023325718/688021155595944081/1b.PNG' + elif args[0][:2].upper() == '2B': + image = 'https://media.discordapp.net/attachments/643871298023325718/688021157496356900/2b.PNG' + elif args[0][:2].upper() == '3B': + image = 'https://media.discordapp.net/attachments/643871298023325718/688021159115096106/3b.PNG' + elif args[0][:2].upper() == 'SS': + image = 'https://media.discordapp.net/attachments/643871298023325718/688021167486795809/ss.PNG' + elif args[0][:2].upper() == 'LF' or args[0][:2].upper() == 'RF': + image = 'https://media.discordapp.net/attachments/643871298023325718/688021163711922216/lf.PNG' + elif args[0][:2].upper() == 'CF': + image = 'https://media.discordapp.net/attachments/643871298023325718/688021162172612638/cf.PNG' + elif args[0][0].upper() == 'C': + image = 'https://media.discordapp.net/attachments/643871298023325718/688021160603942948/c.PNG' + elif args[0][0].upper() == 'P': + image = 'https://media.discordapp.net/attachments/643871298023325718/688021165746552872/p.PNG' + + roll_message = f'```md\n# '\ + f'{d_twenty},{d_six_two + d_six_three},{d_six_one}\n'\ + f'Details:[1d20;2d6;1d6 ({d_twenty} - {d_six_two} {d_six_three} - '\ + f'{d_six_one})]```' + if image: + embed = discord.Embed(title=f'Fielding Chart') + embed.set_image(url=image) + await ctx.send(content=roll_message, embed=embed) + else: + await ctx.channel.send( + content=None, + embed=await self.get_dice_embed(ctx.channel, f'Fielding roll for {ctx.author.name}', roll_message) + ) + + this_roll = { + 'season': self.current['season'], + 'week': self.current['week'], + 'team_id': team["id"] if team else None, + 'roller': ctx.author.id, + 'dsix': d_six_one, + 'twodsix': d_six_two + d_six_three, + 'dtwenty': d_twenty + } + self.rolls.append(this_roll) + + @commands.command(name='i', aliases=['injury'], help='i or injury') + async def injury_roll(self, ctx): + """ + Make an injury check. + """ + team = None + d_twenty = random.randint(1, 20) + + roll_message = f'Injury roll for {ctx.author.name}\n```md\n# {d_twenty}\n'\ + f'Details:[1d20 ({d_twenty})]```' + embed = discord.Embed(title='Injury Chart', description='See https://sombaseball.ddns.net/rules#injuries') + embed.set_image(url='https://sombaseball.ddns.net/static/images/season04/injury-chart.png') + await ctx.channel.send(content=roll_message, embed=embed) + if d_twenty > 8: + await ctx.send(random_salute_gif()) + + this_roll = { + 'season': self.current['season'], + 'week': self.current['week'], + 'team_id': team["id"] if team else None, + 'roller': ctx.author.id, + 'dtwenty': d_twenty + } + self.rolls.append(this_roll) + + @app_commands.command(name='injury', description='Make an injury check; rating = left of "p", games = right') + @app_commands.guilds(discord.Object(id=os.environ.get('GUILD_ID'))) + @app_commands.choices(games=[ + Choice(name='86', value=86), + Choice(name='80', value=80), + Choice(name='70', value=70), + Choice(name='60', value=60), + Choice(name='50', value=50), + Choice(name='40', value=40), + Choice(name='30', value=30), + Choice(name='20', value=20), + ], rating=[ + Choice(name='1', value=1), + Choice(name='2', value=2), + Choice(name='3', value=3), + Choice(name='4', value=4), + Choice(name='5', value=5), + Choice(name='6', value=6), + ]) + async def injury_roll_slash(self, interaction: discord.Interaction, rating: Choice[int], games: Choice[int]): + team = None + + d_six_one = random.randint(1, 6) + d_six_two = random.randint(1, 6) + d_six_three = random.randint(1, 6) + injury_roll = d_six_one + d_six_two + d_six_three + + inj_data = { + 'one': { + 'p86': ['OK', 'OK', 'OK', 'OK', 'OK', 'OK', 'REM', 'REM', 1, 1, 2, 2, 3, 3, 4, 5], + 'p80': [2, 2, 'OK', 'REM', 1, 2, 3, 3, 4, 4, 5, 5, 6, 8, 12, 16], + 'p70': ['OK', 'OK', 'REM', 1, 2, 3, 4, 5, 5, 6, 6, 8, 12, 16, 20, 20], + 'p60': ['OK', 'REM', 1, 2, 3, 4, 5, 6, 6, 8, 12, 12, 16, 20, 20, 'OK'], + 'p50': ['OK', 1, 2, 3, 4, 5, 6, 8, 8, 12, 12, 16, 20, 30, 'REM', 'OK'], + 'p40': ['OK', 5, 1, 3, 5, 6, 8, 12, 12, 16, 20, 30, 4, 2, 'REM', 'OK'], + 'p30': ['OK', 1, 2, 5, 6, 8, 12, 16, 20, 30, 12, 8, 4, 3, 'REM', 'OK'], + 'p20': ['OK', 1, 2, 5, 6, 12, 12, 30, 20, 16, 16, 8, 4, 3, 'REM', 'OK'] + }, + 'two': { + 'p86': [4, 3, 2, 2, 1, 1, 'REM', 'OK', 'REM', 'OK', 2, 1, 2, 2, 3, 5], + 'p80': [12, 6, 4, 2, 2, 'OK', 1, 'OK', 'REM', 1, 'REM', 2, 3, 5, 8, 16], + 'p70': [1, 3, 4, 6, 2, 2, 'OK', 1, 3, 'REM', 4, 5, 8, 12, 16, 3], + 'p60': [5, 'OK', 'OK', 'REM', 1, 2, 5, 3, 4, 6, 4, 8, 12, 16, 16, 'OK'], + 'p50': ['OK', 'OK', 'REM', 1, 2, 3, 4, 5, 6, 5, 8, 12, 16, 20, 20, 'OK'], + 'p40': ['OK', 'REM', 1, 2, 3, 4, 5, 6, 8, 6, 12, 16, 20, 30, 'REM', 'OK'], + 'p30': ['OK', 1, 2, 4, 5, 6, 8, 6, 12, 16, 8, 20, 30, 3, 5, 'REM'], + 'p20': ['OK', 1, 4, 5, 6, 6, 8, 8, 16, 12, 20, 30, 12, 3, 2, 'REM'] + }, + 'three': { + 'p86': [], + 'p80': ['OK', 'OK', 'REM', 1, 3, 'OK', 'REM', 1, 2, 1, 2, 3, 4, 5, 6, 'REM'], + 'p70': ['OK', 6, 'OK', 'REM', 1, 2, 2, 3, 4, 5, 1, 3, 6, 8, 12, 'REM'], + 'p60': ['OK', 'OK', 'REM', 1, 2, 3, 4, 5, 6, 4, 5, 8, 12, 12, 16, 'REM'], + 'p50': ['OK', 1, 1, 2, 3, 4, 5, 6, 8, 6, 8, 12, 12, 16, 5, 'REM'], + 'p40': ['OK', 1, 2, 3, 4, 6, 5, 8, 6, 8, 12, 12, 16, 20, 1, 'REM'], + 'p30': ['OK', 2, 3, 4, 5, 8, 6, 8, 12, 12, 16, 6, 20, 30, 1, 'REM'], + 'p20': ['OK', 1, 2, 4, 5, 12, 12, 8, 6, 16, 8, 20, 30, 3, 5, 'REM'] + }, + 'four': { + 'p86': [], + 'p80': [], + 'p70': ['OK', 'OK', 'REM', 3, 3, 'OK', 'REM', 1, 2, 1, 4, 5, 6, 8, 12, 'REM'], + 'p60': ['OK', 8, 4, 'OK', 'REM', 1, 2, 4, 5, 3, 6, 3, 8, 12, 16, 'REM'], + 'p50': ['OK', 'OK', 'REM', 1, 2, 3, 4, 5, 6, 4, 5, 8, 12, 12, 16, 'REM'], + 'p40': ['OK', 1, 1, 2, 3, 4, 5, 6, 8, 6, 8, 12, 12, 16, 5, 'REM'], + 'p30': ['OK', 'REM', 2, 3, 4, 5, 6, 6, 8, 8, 16, 12, 12, 1, 20, 'REM'], + 'p20': ['OK', 1, 2, 3, 4, 6, 5, 8, 6, 8, 16, 12, 12, 20, 1, 'REM'] + }, + 'five': { + 'p86': [], + 'p80': [], + 'p70': [], + 'p60': ['OK', 'OK', 'REM', 1, 1, 'OK', 'REM', 3, 2, 4, 5, 6, 8, 6, 12, 16], + 'p50': ['OK', 8, 8, 'OK', 1, 3, 2, 4, 5, 6, 'REM', 3, 12, 8, 16, 1], + 'p40': ['OK', 'OK', 'REM', 5, 1, 2, 6, 4, 8, 3, 5, 12, 6, 8, 16, 'REM'], + 'p30': ['OK', 'REM', 2, 3, 5, 4, 6, 5, 8, 6, 12, 8, 3, 1, 16, 'REM'], + 'p20': ['OK', 'REM', 2, 3, 5, 4, 6, 5, 8, 6, 12, 8, 12, 1, 16, 'REM'] + }, + 'six': { + 'p86': [], + 'p80': [], + 'p70': [], + 'p60': [], + 'p50': ['OK', 8, 3, 'OK', 1, 3, 2, 4, 5, 6, 'REM', 8, 12, 3, 16, 1], + 'p40': ['OK', 'OK', 'REM', 5, 1, 3, 8, 4, 6, 2, 5, 12, 3, 4, 16, 'REM'], + 'p30': ['OK', 'REM', 4, 5, 2, 3, 6, 4, 8, 5, 12, 6, 3, 1, 16, 'REM'], + 'p20': ['OK', 'REM', 2, 3, 5, 4, 6, 5, 8, 6, 12, 8, 12, 1, 16, 'REM'] + } + } + p_ratings = ['one', 'two', 'three', 'four', 'five', 'six'] + + injury_string = f'```md\n# {injury_roll}\n' \ + f'Details:[3d6 ({d_six_one} {d_six_two} {d_six_three})]\n```\n' + + logging.info(f'injury rating: {rating.value}p{games.value}') + + injury_list = inj_data[p_ratings[rating.value - 1]][f'p{games.value}'] + injury_result = injury_list[injury_roll - 3] + logging.info(f'injury rating: {rating.value}p{games.value} / array: {injury_list}[{injury_roll - 2}] / result: {injury_result}') + + if isinstance(injury_result, int): + await interaction.response.send_message(random_salute_gif()) + injury_string += f'With a roll of {injury_roll}, the injury length is **{injury_result} ' \ + f'game{"s" if injury_result > 1 else ""}**.' + elif injury_result == 'REM': + await interaction.response.send_message(random_salute_gif()) + injury_string += f'With a roll of {injury_roll}, the injury length is **REMAINDER OF GAME** for batters ' \ + f'or **FATIGUED** for pitchers' + else: + await interaction.response.send_message(random_conf_gif()) + injury_string += f'With a roll of {injury_roll}, the player is **OKAY** - no injury!' + + embed = await self.get_dice_embed( + interaction.channel.name, + f'Injury roll for {interaction.user.name}', + injury_string + ) + embed.set_footer(text='For pitchers, add their current rest to the injury') + + await interaction.channel.send(content=None, embed=embed) + + this_roll = { + 'season': self.current['season'], + 'week': self.current['week'], + 'team_id': team["id"] if team else None, + 'roller': interaction.user.id, + 'threedsix': d_six_one + d_six_two + d_six_three + } + self.rolls.append(this_roll) + + @commands.command(name='c', aliases=['chaos', 'choas'], help='c, chaos, or choas') + async def chaos_roll(self, ctx): + """ + Have the pitcher check for chaos with a runner on base. + """ + await react_and_reply( + ctx, + '❌', + 'This command has been deprecated. It is now implemented within the `!ab` command. If you need manual ' + 'chaos, use the Paper Dynasty bot.' + ) + return + + d_twenty = random.randint(1, 20) + d_twenty_two = random.randint(1, 20) + flag = None + + if d_twenty == 1: + flag = 'wild pitch' + elif d_twenty == 2: + if random.randint(1, 2) == 1: + flag = 'balk' + else: + flag = 'passed ball' + + if not flag: + roll_message = f'Chaos roll for {ctx.author.name}\n```md\nNo Chaos```' + else: + roll_message = f'Chaos roll for {ctx.author.name}\n```md\nCheck {flag}```\n'\ + f'{flag.title()} roll```md\n# {d_twenty_two}\nDetails: [1d20 ({d_twenty_two})]```' + + await ctx.channel.send(roll_message) + this_roll = { + 'season': self.current['season'], + 'week': self.current['week'], + 'team_id': team["id"] if team else None, + 'roller': ctx.author.id, + 'dtwenty': d_twenty + } + self.rolls.append(this_roll) + + @commands.command(name='j', aliases=['jump'], help='j or jump') + async def jump_roll(self, ctx): + """ + Check for a baserunner's jump before stealing. + """ + team = None + d_six_one = random.randint(1, 6) + d_six_two = random.randint(1, 6) + d_twenty = random.randint(1, 20) + d_twenty_two = random.randint(1, 20) + flag = None + + if d_twenty == 1: + flag = 'pickoff' + elif d_twenty == 2: + flag = 'balk' + + if not flag: + roll_message = f'```md\n# {d_six_one + d_six_two}\n'\ + f'Details:[2d6 ({d_six_one} {d_six_two})]```' + else: + roll_message = f'```md\nCheck {flag}```\n' \ + f'{flag.title()} roll```md\n# {d_twenty_two}\nDetails: [1d20 ({d_twenty_two})]```' + + await ctx.channel.send( + content=None, + embed=await self.get_dice_embed(ctx.channel, f'Jump roll for {ctx.author.name}', roll_message) + ) + this_roll = { + 'season': self.current['season'], + 'week': self.current['week'], + 'team_id': team["id"] if team else None, + 'roller': ctx.author.id, + 'twodsix': d_six_two + d_six_one, + 'dtwenty': d_twenty + } + self.rolls.append(this_roll) + + @commands.command(name='f', aliases=['safielding', 'saf'], help='f or safielding') + async def sa_fielding_roll(self, ctx, *args): + """ + Make a Super Advanced fielding check. + """ + team = None + d_twenty = random.randint(1, 20) + d_six_one = random.randint(1, 6) + d_six_two = random.randint(1, 6) + d_six_three = random.randint(1, 6) + error_dice = d_six_one + d_six_two + d_six_three + x_chart = None + error_chart = 'https://sombaseball.ddns.net/static/images/season04/error-' + range_note = None + error_note = None + error_note1 = None + error_note2 = None + symbol_link = 'https://docs.google.com/document/d/1wu63XSgfQE2wadiegWaaDda11QvqkN0liRurKm0vcTs/edit' \ + '#heading=h.7oj0v3t1kc6o' + + if args: + if args[0][:2].upper() == '1B' or args[0][:2].upper() == '2B' or args[0][:2].upper() == '3B' \ + or args[0][:2].upper() == 'SS': + x_chart = 'https://sombaseball.ddns.net/static/images/season04/range-infield.png' + + # Build range note + if d_twenty == 1: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + 'G3# SI1 ----SI2----\n' \ + '```\n\n' + elif d_twenty == 2: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + 'G2# SI1 ----SI2----\n' \ + '```\n\n' + elif d_twenty <= 4: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + 'G2# G3# SI1 --SI2--\n' \ + '```\n\n' + elif d_twenty == 5: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + 'G1 --G3#-- SI1 SI2\n' \ + '```\n\n' + elif d_twenty == 6: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + 'G1 G2# G3# SI1 SI2\n' \ + '```\n\n' + elif d_twenty <= 8: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + 'G1 G2 --G3#-- SI1\n' \ + '```\n\n' + elif d_twenty == 9: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + 'G1 G2 G3 --G3#--\n' \ + '```\n\n' + elif d_twenty == 10: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '--G1--- G2 --G3#--\n' \ + '```\n\n' + elif d_twenty <= 12: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '--G1--- G2 G3 G3#\n' \ + '```\n\n' + elif d_twenty == 13: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '--G1--- G2 --G3---\n' \ + '```\n\n' + elif d_twenty == 14: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '--G1--- --G2--- G3\n' \ + '```\n\n' + elif d_twenty <= 16: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '----G1----- G2 G3\n' \ + '```\n\n' + elif d_twenty == 17: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '------G1------- G3\n' \ + '```\n\n' + elif d_twenty <= 19: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '------G1------- G2\n' \ + '```\n\n' + elif d_twenty == 20: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '--------G1---------\n' \ + '```\n\n' + + if args[0][:2].upper() == '1B': + error_chart += 'first-base.png' + + if error_dice == 18: + error_note = '2-base error for e3 -> e12, e19 -> e28\n' \ + '1-base error for e1, e2, e30' + elif error_dice == 17: + error_note = '2-base error for e13 -> e28\n' \ + '1-base error for e1, e5, e8, e9, e29' + elif error_dice == 16: + error_note = '2-base error for e29, e30\n' \ + '1-base error for e2, e8, e16, e19, e23' + elif error_dice == 15: + error_note = '1-base error for e3, e8, e10 -> e12, e20, e26, e30' + elif error_dice == 14: + error_note = '1-base error for e4, e5, e9, e15, e18, e22, e24 -> e28' + elif error_dice == 13: + error_note = '1-base error for e6, e13, e24, e26 -> e28, e30' + elif error_dice == 12: + error_note = '1-base error for e14 -> e18, e21 -> e26, e28 -> e30' + elif error_dice == 11: + error_note = '1-base error for e10, e13, e16 -> e20, e23 -> e25, e27 -> e30' + elif error_dice == 10: + error_note = '1-base error for e19 -> e21, e23, e29' + elif error_dice == 9: + error_note = '1-base error for e7, e12, e14, e21, e25, e26, e29' + elif error_dice == 8: + error_note = '1-base error for e11, e27' + elif error_dice == 7: + error_note = '1-base error for e9, e15, e22, e27, e28' + elif error_dice == 6: + error_note = '1-base error for e8, e11, e12, e17, e20' + elif error_dice == 5: + error_note = f'Rare play!\n\n' \ + f'**G3**: {INFIELD_X_CHART["g3"]["rp"]}\n' \ + f'**G2**: {INFIELD_X_CHART["g2"]["rp"]}\n' \ + f'**G1**: {INFIELD_X_CHART["g1"]["rp"]}\n' \ + f'**SI1**: {INFIELD_X_CHART["si1"]["rp"]}\n' \ + f'**SI2**: {OUTFIELD_X_CHART["si2"]["rp"]}\n' + elif error_dice == 4: + error_note = 'No error' + elif error_dice == 3: + error_note = '2-base error for e8 -> e12, e24 -> e28\n' \ + '1-base error for e2, e3, e6, e7, e14, e16, e17, e21' + if args[0][:2].upper() == '2B': + error_chart += 'second-base.png' + + if error_dice == 18: + error_note = '2-base error for e4 -> e19, e28 -> e41, e53 -> e65\n' \ + '1-base error for e22, e24, e25, e27, e44, e50' + elif error_dice == 17: + error_note = '2-base error for e20 -> e41, e68, e71\n' \ + '1-base error for e3, e4, e8 -> e12, e15, e16, e19' + elif error_dice == 16: + error_note = '2-base error for e53 -> 71\n' \ + '1-base error for e5 -> 10, e14, e16, e29, e37' + elif error_dice == 15: + error_note = '1-base error for e11, e12, e14, e16, e17, e19, e26 -> e28, e30, e32, e37, ' \ + 'e50 -> e62, e71' + elif error_dice == 14: + error_note = '1-base error for e13, e15, e34, e47, e65' + elif error_dice == 13: + error_note = '1-base error for e18, e20, e21, e26 -> e28, e39, e41, e50, e56, e59, e65, e71' + elif error_dice == 12: + error_note = '1-base error for e22, e30, e34, e39, e44, e47, e53, e56, e62, e68, e71' + elif error_dice == 11: + error_note = '1-base error for e23 -> e25, e29, e32, e37, e41, e50, e53, e59, e62, e68' + elif error_dice == 10: + error_note = '1-base error for e68' + elif error_dice == 9: + error_note = '1-base error for e44' + elif error_dice == 8: + error_note = 'No error' + elif error_dice == 7: + error_note = '1-base error for e47, e65' + elif error_dice == 6: + error_note = '1-base error for e17, e19, e56 -> 62' + elif error_dice == 5: + error_note = f'Rare play!\n\n' \ + f'**G3**: {INFIELD_X_CHART["g3"]["rp"]}\n' \ + f'**G2**: {INFIELD_X_CHART["g2"]["rp"]}\n' \ + f'**G1**: {INFIELD_X_CHART["g1"]["rp"]}\n' \ + f'**SI1**: {INFIELD_X_CHART["si1"]["rp"]}\n' \ + f'**SI2**: {OUTFIELD_X_CHART["si2"]["rp"]}\n' + elif error_dice == 4: + error_note = '1-base error for e10, e21' + elif error_dice == 3: + error_note = '2-base error for e12 -> e19, e37 -> e41, e59 -> e65\n' \ + '1-base error for e2 -> e4, e6, e20, e25, e28, e29' + if args[0][:2].upper() == '3B': + error_chart += 'third-base.png' + + if error_dice == 18: + error_note = '2-base error for e1, e2, e8, e10, e13 -> e16, e21 -> e24, e27, e30 -> e33, e50, ' \ + 'e56, e62, e65\n' \ + '1-base error for e39' + elif error_dice == 17: + error_note = '2-base error for e3 -> e10, e17, e18, e25 -> e27, e34 -> e37, e44, e47\n' \ + '1-base error for e11, e19, e32, e56' + elif error_dice == 16: + error_note = '2-base error for e11 -> e18, e32, e33, e37, e53, e62, e65\n' \ + '1-base error for e4, e8, e19, e21, e22, e27, e41' + elif error_dice == 15: + error_note = '2-base error for e19 -> 27, e32, e33, e37, e39, e44, e50, e59\n' \ + '1-base error for e5 -> e8, e11, e14, e15, e17, e18, e28 -> e31, e34' + elif error_dice == 14: + error_note = '2-base error for e28 -> e31, e34, e35, e50\n' \ + '1-base error for e14, e16, e19, e20, e22, e32, e39, e44, e56, e62' + elif error_dice == 13: + error_note = '2-base error for e41, e47, e53, e59\n' \ + '1-base error for e10, e15, e23, e25, e28, e30, e32, e33, e35, e44, e65' + elif error_dice == 12: + error_note = '2-base error for e62\n' \ + '1-base error for e12, e17, e22, e24, e27, e29, e34 -> e50, e56 -> e59, e65' + elif error_dice == 11: + error_note = '2-base error for e56, e65\n' \ + '1-base error for e13, e18, e20, e21, e23, e26, e28, e31 -> e33, e35, e37, ' \ + 'e41 -> e53, e59, e65' + elif error_dice == 10: + error_note = '1-base error for e26, e31, e41, e53 -> 65' + elif error_dice == 9: + error_note = '1-base error for e24, e27, e29, e34, e37, e39, e47 -> e65' + elif error_dice == 8: + error_note = '1-base error for e25, e30, e33, e47, e53, e56, e62, e65' + elif error_dice == 7: + error_note = '1-base error for e16, e19, e39, e59 -> e65' + elif error_dice == 6: + error_note = '1-base error for e21, e25, e30, e34, e53' + elif error_dice == 5: + error_note = f'Rare play!\n\n' \ + f'**G3**: {INFIELD_X_CHART["g3"]["rp"]}\n' \ + f'**G2**: {INFIELD_X_CHART["g2"]["rp"]}\n' \ + f'**G1**: {INFIELD_X_CHART["g1"]["rp"]}\n' \ + f'**SI1**: {INFIELD_X_CHART["si1"]["rp"]}\n' \ + f'**SI2**: {OUTFIELD_X_CHART["si2"]["rp"]}\n' + elif error_dice == 4: + error_note = '1-base error for e2, e3, e6, e14, e16, e44' + elif error_dice == 3: + error_note = '2-base error for e10, e15, e16, e23, e24, e56\n' \ + '1-base error for e1 -> e4, e8, e14' + if args[0][:2].upper() == 'SS': + error_chart += 'shortstop.png' + + if error_dice == 18: + error_note = '2-base error for e4 -> e12, e22 -> e32, e40 -> e48, e64, e68\n' \ + '1-base error for e1, e18, e34, e52, e56' + elif error_dice == 17: + error_note = '2-base error for e14 -> 32, e52, e56, e72 -> e84\n' \ + '1-base error for e3 -> e5, e8 ,e10, e36' + elif error_dice == 16: + error_note = '2-base error for e33 -> 56, e72\n' \ + '1-base error for e6 -> e10, e17, e18, e20, e28, e31, e88' + elif error_dice == 15: + error_note = '2-base error for e60 -> e68, e76 -> 84\n' \ + '1-base error for e12, e14, e17, e18, e20 -> e22, e24, e28, e31 -> 36, e40, ' \ + 'e48, e72' + elif error_dice == 14: + error_note = '1-base error for e16, e19, e38, e42, e60, e68' + elif error_dice == 13: + error_note = '1-base error for e23, e25, e32 -> 38, e44, e52, e72 -> 84' + elif error_dice == 12: + error_note = '1-base error for e26, e27, e30, e42, e48, e56, e64, e68, e76 -> e88' + elif error_dice == 11: + error_note = '1-base error for e29, e40, e52 -> e60, e72, e80 -> e88' + elif error_dice == 10: + error_note = '1-base error for e84' + elif error_dice == 9: + error_note = '1-base error for e64, e68, e76, e88' + elif error_dice == 8: + error_note = '1-base error for e44' + elif error_dice == 7: + error_note = '1-base error for e60' + elif error_dice == 6: + error_note = '1-base error for e21, e22, e24, e28, e31, e48, e64, e72' + elif error_dice == 5: + error_note = f'Rare play!\n\n' \ + f'**G3**: {INFIELD_X_CHART["g3"]["rp"]}\n' \ + f'**G2**: {INFIELD_X_CHART["g2"]["rp"]}\n' \ + f'**G1**: {INFIELD_X_CHART["g1"]["rp"]}\n' \ + f'**SI1**: {INFIELD_X_CHART["si1"]["rp"]}\n' \ + f'**SI2**: {OUTFIELD_X_CHART["si2"]["rp"]}\n' + elif error_dice == 4: + error_note = '2-base error for e72\n' \ + '1-base error for e14, e19, e20, e24, e25, e31, e80' + elif error_dice == 3: + error_note = '2-base error for e10, e12, e28 -> e32, e48, e84\n' \ + '1-base error for e2, e5, e7, e23, e27' + + elif args[0][:2].upper() == 'LF' or args[0][:2].upper() == 'RF' or args[0][:2].upper() == 'CF': + x_chart = 'https://sombaseball.ddns.net/static/images/season04/range-outfield.png' + + # Build range note + if d_twenty == 1: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + 'F1 DO2 DO3 --TR3--\n' \ + '```\n\n' + elif d_twenty == 2: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + 'F2 SI2 DO2 DO3 TR3\n' \ + '```\n\n' + elif d_twenty == 3: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + 'F2 SI2 --DO2-- DO3\n' \ + '```\n\n' + elif d_twenty == 4: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + 'F2 F1 SI2 DO2 DO3\n' \ + '```\n\n' + elif d_twenty <= 6: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '--F2--- --SI2-- DO2\n' \ + '```\n\n' + elif d_twenty == 7: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '--F2--- F1 SI2 DO2\n' \ + '```\n\n' + elif d_twenty == 8: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '--F2--- F1 --SI2--\n' \ + '```\n\n' + elif d_twenty <= 11: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '----F2----- --SI2--\n' \ + '```\n\n' + elif d_twenty <= 13: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '----F2----- F1 SI2\n' \ + '```\n\n' + elif d_twenty <= 15: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + 'F3 ----F2----- SI2\n' \ + '```\n\n' + elif d_twenty == 16: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '--F3--- --F2--- F1\n' \ + '```\n\n' + elif d_twenty <= 18: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '----F3----- F2 F1\n' \ + '```\n\n' + elif d_twenty == 19: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '------F3------- F2\n' \ + '```\n\n' + elif d_twenty == 20: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '--------F3---------\n' \ + '```\n\n' + + if args[0][:2].upper() == 'LF' or args[0][:2].upper() == 'RF': + error_chart += 'corner-outfield.png' + + if error_dice == 18: + error_note = '3-base error for e4 -> e12, e19 -> e25\n' \ + '2-base error for e18\n' \ + '1-base error for e2, e3, e15' + elif error_dice == 17: + error_note = '3-base error for e13 -> e25\n' \ + '2-base error for e1, e6, e8, e10' + elif error_dice == 16: + error_note = '2-base error for e2\n' \ + '1-base error for e7 -> 12, e22, e24, e25' + elif error_dice == 15: + error_note = '2-base error for e3, e4, e7, e8, e10, e11, e13, e20, e21' + elif error_dice == 14: + error_note = '2-base error for e5, e6, e10, e12, e14, e15, e22, e23' + elif error_dice == 13: + error_note = '2-base error for e11, e12, e16, e20, e24, e25' + elif error_dice == 12: + error_note = '2-base error for e13 -> e18, e21 -> e23, e25' + elif error_dice == 11: + error_note = '2-base error for e9, e18 -> e21, e23 -> e25' + elif error_dice == 10: + error_note = '2-base error for e19' + elif error_dice == 9: + error_note = '2-base error for e22' + elif error_dice == 8: + error_note = '2-base error for e24' + elif error_dice == 7: + error_note = '1-base error for e19 -> e21, e23' + elif error_dice == 6: + error_note = '2-base error for e7, e8\n' \ + '1-base error for e13 -> e18, e22, e24, e25' + elif error_dice == 5: + error_note = f'Rare play!' + error_note1 = f'**F1**: {OUTFIELD_X_CHART["f1"]["rp"]}\n' \ + f'**F2**: {OUTFIELD_X_CHART["f2"]["rp"]}\n' \ + f'**F3**: {OUTFIELD_X_CHART["f3"]["rp"]}\n' + error_note2 = f'**SI2**: {OUTFIELD_X_CHART["si2"]["rp"]}\n' \ + f'**DO2**: {OUTFIELD_X_CHART["do2"]["rp"]}\n' \ + f'**DO3**: {OUTFIELD_X_CHART["do3"]["rp"]}\n' \ + f'**TR3**: {OUTFIELD_X_CHART["tr3"]["rp"]}\n' + elif error_dice == 4: + error_note = '2-base error for e13, e16, e20, e23 -> e25\n' \ + '1-base error for e4 -> e6, e10 -> 12, e16 -> e18' + elif error_dice == 3: + error_note = '3-base error for e8 -> e12, e24, e25\n' \ + '2-base error for e19\n' \ + '1-base error for e1 -> e3, e6, e14, e15, e18, e20 -> e22' + else: + error_chart += 'center-field.png' + + if error_dice == 18: + error_note = '3-base error for e4 -> 19\n' \ + '2-base error for e2, e25\n' \ + '1-base error for e3, e23' + elif error_dice == 17: + error_note = '3-base error for e20 -> e25\n' \ + '2-base error for e1, e2, e5, e7, e9, e13 -> e15, e17' + elif error_dice == 16: + error_note = '2-base error for e3 -> e5, e8, e23\n' \ + '1-base error for e10 -> e18' + elif error_dice == 15: + error_note = '2-base error for e6 -> e8, e12, e13, e19' + elif error_dice == 14: + error_note = '2-base error for e9, e10, e16 -> e18, e20 -> e23' + elif error_dice == 13: + error_note = '2-base error for e11, e18, e20, e23 -> e25' + elif error_dice == 12: + error_note = '2-base error for e14, e15, e21, e22, e24' + elif error_dice == 11: + error_note = '2-base error for e19, e25' + elif error_dice >= 8: + error_note = 'No error' + elif error_dice == 7: + error_note = '2-base error for e16, e17' + elif error_dice == 6: + error_note = '2-base error for e12, e13\n' \ + '1-base error for e19 -> e25' + elif error_dice == 5: + error_note = f'Rare play!' + error_note1 = f'**F1**: {OUTFIELD_X_CHART["f1"]["rp"]}\n' \ + f'**F2**: {OUTFIELD_X_CHART["f2"]["rp"]}\n' \ + f'**F3**: {OUTFIELD_X_CHART["f3"]["rp"]}\n' + error_note2 = f'**SI2**: {OUTFIELD_X_CHART["si2"]["rp"]}\n' \ + f'**DO2**: {OUTFIELD_X_CHART["do2"]["rp"]}\n' \ + f'**DO3**: {OUTFIELD_X_CHART["do3"]["rp"]}\n' \ + f'**TR3**: {OUTFIELD_X_CHART["tr3"]["rp"]}\n' + elif error_dice == 4: + error_note = '2-base error for e10, e12, e13, e20, e22, e23\n' \ + '1-base error for e3 -> e9, e15 -> e18' + elif error_dice == 3: + error_note = '3-base error for e12 -> e19\n' \ + '2-base error for e10, e11\n' \ + '1-base error for e2, e3, e7 -> e9, e21 -> e23' + + elif args[0][0].upper() == 'C': + x_chart = 'https://sombaseball.ddns.net/static/images/season04/range-catcher.png' + error_chart += 'catcher.png' + + # Build range note + if d_twenty == 1: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + 'G3 ------SI1------\n' \ + '```\n' + elif d_twenty == 2: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + 'G3 SPD ----SI1----\n' \ + '```\n' + elif d_twenty == 3: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '--G3--- SPD --SI1--\n' \ + '```\n' + elif d_twenty == 4: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + 'G2 G3 --SPD-- SI1\n' \ + '```\n' + elif d_twenty == 5: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + 'G2 --G3--- --SPD--\n' \ + '```\n' + elif d_twenty == 6: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '--G2--- G3 --SPD--\n' \ + '```\n' + elif d_twenty == 7: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + 'PO G2 G3 --SPD--\n' \ + '```\n' + elif d_twenty == 8: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + 'PO --G2--- G3 SPD\n' \ + '```\n' + elif d_twenty == 9: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '--PO--- G2 G3 SPD\n' \ + '```\n' + elif d_twenty == 10: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + 'FO PO G2 G3 SPD\n' \ + '```\n' + elif d_twenty == 11: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + 'FO --PO--- G2 G3\n' \ + '```\n' + elif d_twenty == 12: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '--FO--- PO G2 G3\n' \ + '```\n' + elif d_twenty == 13: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + 'G1 FO PO G2 G3\n' \ + '```\n' + elif d_twenty == 14: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + 'G1 --FO--- PO G2\n' \ + '```\n' + elif d_twenty <= 16: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '--G1--- FO PO G2\n' \ + '```\n' + elif d_twenty <= 18: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '----G1----- FO PO\n' \ + '```\n' + elif d_twenty == 19: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '----G1----- --FO---\n' \ + '```\n' + elif d_twenty == 20: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '------G1------- FO\n' \ + '```\n' + + # Build error note + # error_dice = 5 + if error_dice == 18: + error_note = '2-base error for e4 -> 16\n1-base error for e2, e3' + elif error_dice == 17: + error_note = '1-base error for e1, e2, e4, e5, e12 -> e14, e16' + elif error_dice == 16: + error_note = '1-base error for e3 -> e5, e7, e12 -> e14, e16' + elif error_dice == 15: + error_note = '1-base error for e7, e8, e12, e13, e15' + elif error_dice == 14: + error_note = '1-base error for e6' + elif error_dice == 13: + error_note = '1-base error for e9' + elif error_dice == 12: + error_note = '1-base error for e10, e14' + elif error_dice == 11: + error_note = '1-base error for e11, e15' + elif 8 <= error_dice <= 10: + error_note = 'No error' + elif error_dice == 7: + error_note = '1-base error for e16' + elif error_dice == 6: + error_note = '1-base error for e8, e12, e13' + elif error_dice == 5: + error_note = 'Rare play! Ignore range chart above and consult ranges below\n\n' + error_note1 = '__If Bases Empty__\n' + if d_twenty == 1: + error_note1 += f'**c-1**: {INFIELD_X_CHART["g3"]["rp"]}\n' \ + f'**c-2 -> 5**: {INFIELD_X_CHART["si1"]["rp"]}\n\n' + elif d_twenty == 2: + error_note1 += f'**c-1 -> 2**: {INFIELD_X_CHART["g3"]["rp"]}\n' \ + f'**c-3 -> 5**: {INFIELD_X_CHART["si1"]["rp"]}\n\n' + elif d_twenty == 3: + error_note1 += f'**c-1**: {INFIELD_X_CHART["po"]["rp"]}\n' \ + f'**c-2 -> 3**: {INFIELD_X_CHART["g3"]["rp"]}\n' \ + f'**c-4 -> 5**: {INFIELD_X_CHART["si1"]["rp"]}\n\n' + elif d_twenty == 4: + error_note1 += f'**c-1 -> 2**: {INFIELD_X_CHART["po"]["rp"]}\n' \ + f'**c-3 -> 4**: {INFIELD_X_CHART["g3"]["rp"]}\n' \ + f'**c-5**: {INFIELD_X_CHART["si1"]["rp"]}\n\n' + elif d_twenty == 5: + error_note1 += f'**c-1 -> 3**: {INFIELD_X_CHART["po"]["rp"]}\n' \ + f'**c-4 -> 5**: {INFIELD_X_CHART["g3"]["rp"]}\n\n' + elif d_twenty == 6: + error_note1 += f'**c-1 -> 4**: {INFIELD_X_CHART["po"]["rp"]}\n' \ + f'**c-5**: {INFIELD_X_CHART["g3"]["rp"]}\n\n' + elif d_twenty <= 10: + error_note1 += f'**c-1 -> 5**: {INFIELD_X_CHART["po"]["rp"]}\n\n' + elif d_twenty == 11: + error_note1 += f'**c-1**: {INFIELD_X_CHART["fo"]["rp"]}\n' \ + f'**c-2 -> 5**: {INFIELD_X_CHART["po"]["rp"]}\n\n' + elif d_twenty == 12: + error_note1 += f'**c-1 -> 2**: {INFIELD_X_CHART["fo"]["rp"]}\n' \ + f'**c-3 -> 5**: {INFIELD_X_CHART["po"]["rp"]}\n\n' + elif d_twenty == 13: + error_note1 += f'**c-1 -> 3**: {INFIELD_X_CHART["fo"]["rp"]}\n' \ + f'**c-4 -> 5**: {INFIELD_X_CHART["po"]["rp"]}\n\n' + elif d_twenty == 14: + error_note1 += f'**c-1 -> 4**: {INFIELD_X_CHART["fo"]["rp"]}\n' \ + f'**c-5**: {INFIELD_X_CHART["po"]["rp"]}\n\n' + elif d_twenty <= 16: + error_note1 += f'**c-1 -> 5**: {INFIELD_X_CHART["fo"]["rp"]}\n\n' + elif d_twenty == 17: + error_note1 += f'**c-1 -> 2**: {INFIELD_X_CHART["g2"]["rp"]}\n' \ + f'**c-3**: {INFIELD_X_CHART["g3"]["rp"]}\n' \ + f'**c-4 -> 5**: {INFIELD_X_CHART["fo"]["rp"]}\n\n' + elif d_twenty == 18: + error_note1 += f'**c-1 -> 2**: {INFIELD_X_CHART["g2"]["rp"]}\n' \ + f'**c-3 -> 4**: {INFIELD_X_CHART["g3"]["rp"]}\n' \ + f'**c-5**: {INFIELD_X_CHART["fo"]["rp"]}\n\n' + elif d_twenty == 19: + error_note1 += f'**c-1 -> 2**: {INFIELD_X_CHART["g1"]["rp"]}\n' \ + f'**c-3**: {INFIELD_X_CHART["g2"]["rp"]}\n' \ + f'**c-4 -> 5**: {INFIELD_X_CHART["g3"]["rp"]}\n\n' + elif d_twenty == 20: + error_note1 += f'**c-1 -> 2**: {INFIELD_X_CHART["g1"]["rp"]}\n' \ + f'**c-3 -> 4**: {INFIELD_X_CHART["g2"]["rp"]}\n' \ + f'**c-5**: {INFIELD_X_CHART["g3"]["rp"]}\n\n' + + error_note2 = '__If Runners on Base__\n' + if d_twenty <= 2: + error_note2 += f'**c-1 -> 5**: {INFIELD_X_CHART["wp"]["rp"]}\n\n' + elif d_twenty == 3: + error_note2 += f'**c-1**: {INFIELD_X_CHART["x"]["rp"]}\n' \ + f'**c-2 -> 5**: {INFIELD_X_CHART["wp"]["rp"]}\n\n' + elif d_twenty == 4: + error_note2 += f'**c-1 -> 2**: {INFIELD_X_CHART["x"]["rp"]}\n' \ + f'**c-3 -> 5**: {INFIELD_X_CHART["wp"]["rp"]}\n\n' + elif d_twenty == 5: + error_note2 += f'**c-1 -> 3**: {INFIELD_X_CHART["x"]["rp"]}\n' \ + f'**c-4 -> 5**: {INFIELD_X_CHART["wp"]["rp"]}\n\n' + elif d_twenty == 6: + error_note2 += f'**c-1 -> 4**: {INFIELD_X_CHART["x"]["rp"]}\n' \ + f'**c-5**: {INFIELD_X_CHART["wp"]["rp"]}\n\n' + elif d_twenty <= 9: + error_note2 += f'**c-1**: {INFIELD_X_CHART["po"]["rp"]}\n' \ + f'**c-2 -> 5**: {INFIELD_X_CHART["x"]["rp"]}\n\n' + elif d_twenty <= 12: + error_note2 += f'**c-1 -> 2**: {INFIELD_X_CHART["po"]["rp"]}\n' \ + f'**c-3 -> 5**: {INFIELD_X_CHART["x"]["rp"]}\n\n' + elif d_twenty == 13: + error_note2 += f'**c-1**: {INFIELD_X_CHART["fo"]["rp"]}\n' \ + f'**c-2**: {INFIELD_X_CHART["po"]["rp"]}\n' \ + f'**c-3 -> 5**: {INFIELD_X_CHART["x"]["rp"]}\n\n' + elif d_twenty == 14: + error_note2 += f'**c-1 -> 2**: {INFIELD_X_CHART["fo"]["rp"]}\n' \ + f'**c-3**: {INFIELD_X_CHART["po"]["rp"]}\n' \ + f'**c-4 -> 5**: {INFIELD_X_CHART["x"]["rp"]}\n\n' + elif d_twenty <= 16: + error_note2 += f'**c-1 -> 3**: {INFIELD_X_CHART["fo"]["rp"]}\n' \ + f'**c-4 -> 5**: {INFIELD_X_CHART["x"]["rp"]}\n\n' + elif d_twenty == 17: + error_note2 += f'**c-1 -> 2**: {INFIELD_X_CHART["g2"]["rp"]}\n' \ + f'**c-3**: {INFIELD_X_CHART["g3"]["rp"]}\n' \ + f'**c-4**: {INFIELD_X_CHART["po"]["rp"]}\n' \ + f'**c-5**: {INFIELD_X_CHART["x"]["rp"]}\n\n' + elif d_twenty == 18: + error_note2 += f'**c-1 -> 2**: {INFIELD_X_CHART["g2"]["rp"]}\n' \ + f'**c-3 -> 4**: {INFIELD_X_CHART["g3"]["rp"]}\n' \ + f'**c-5**: {INFIELD_X_CHART["x"]["rp"]}\n\n' + elif d_twenty == 19: + error_note2 += f'**c-1**: {INFIELD_X_CHART["g1"]["rp"]}\n' \ + f'**c-2 -> 3**: {INFIELD_X_CHART["g2"]["rp"]}\n' \ + f'**c-4 -> 5**: {INFIELD_X_CHART["g3"]["rp"]}\n\n' + elif d_twenty == 20: + error_note2 += f'**c-1 -> 2**: {INFIELD_X_CHART["g1"]["rp"]}\n' \ + f'**c-3 -> 4**: {INFIELD_X_CHART["g2"]["rp"]}\n' \ + f'**c-5**: {INFIELD_X_CHART["g3"]["rp"]}\n\n' + elif error_dice == 4: + error_note = '1-base error for e5, e13' + else: + error_note = '2-base error for e12 -> e16\n1-base error for e2, e3, e7, e11' + elif args[0][0].upper() == 'P': + x_chart = 'https://sombaseball.ddns.net/static/images/season04/range-pitcher.png' + error_chart += 'pitcher.png' + + # Build range note + if d_twenty <= 2: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + 'G3 ------SI1------\n' \ + '```\n' + elif d_twenty == 3: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '--G3--- ----SI1----\n' \ + '```\n' + elif d_twenty == 4: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '----G3----- --SI1--\n' \ + '```\n' + elif d_twenty <= 6: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '------G3------- SI1\n' \ + '```\n' + elif d_twenty == 7: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '--------G3---------\n' \ + '```\n' + elif d_twenty <= 9: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + 'G2 ------G3-------\n' \ + '```\n' + elif d_twenty <= 12: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + 'G1 G2 ----G3-----\n' \ + '```\n' + elif d_twenty == 13: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '--G1--- G2 --G3---\n' \ + '```\n' + elif d_twenty == 14: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '--G1--- --G2--- G3\n' \ + '```\n' + elif d_twenty <= 16: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '--G1--- ----G2-----\n' \ + '```\n' + elif d_twenty <= 18: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '----G1----- --G2---\n' \ + '```\n' + elif d_twenty == 19: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '------G1------- G2\n' \ + '```\n' + elif d_twenty == 20: + range_note = '```\n' \ + ' 1 | 2 | 3 | 4 | 5\n' \ + '--------G1---------\n' \ + '```\n' + + # Build error note + if error_dice == 18: + error_note = '2-base error for e4 -> e12, e19 -> e28, e34 -> e43, e46 -> e48' + elif error_dice == 17: + error_note = '2-base error for e13 -> e28, e44 -> e50' + elif error_dice == 16: + error_note = '2-base error for e34 -> e48, e50, e51\n' \ + '1-base error for e8, e11, e16, e23' + elif error_dice == 15: + error_note = '2-base error for e50, e51\n' \ + '1-base error for e10 -> e12, e19, e20, e24, e26, e30, e35, e38, e40, e46, e47' + elif error_dice == 14: + error_note = '1-base error for e4, e14, e18, e21, e22, e26, e31, e35, e42, e43, e48 -> e51' + elif error_dice == 13: + error_note = '1-base error for e6, e13, e14, e21, e22, e26, e27, e30 -> 34, e38 -> e51' + elif error_dice == 12: + error_note = '1-base error for e7, e11, e12, e15 -> e19, e22 -> e51' + elif error_dice == 11: + error_note = '1-base error for e10, e13, e15, e17, e18, e20, e21, e23, e24, e27 -> 38, e40, e42, ' \ + 'e44 -> e51' + elif error_dice == 10: + error_note = '1-base error for e20, e23, e24, e27 -> e51' + elif error_dice == 9: + error_note = '1-base error for e16, e19, e26, e28, e34 -> e36, e39 -> e51' + elif error_dice == 8: + error_note = '1-base error for e22, e33, e38, e39, e43 -> e51' + elif error_dice == 7: + error_note = '1-base error for e14, e21, e36, e39, e42 -> e44, e47 -> e51' + elif error_dice == 6: + error_note = '1-base error for e8, e22, e38, e39, e43 -> e51' + elif error_dice == 5: + error_note = f'Rare play!\n\n' \ + f'**G3**: {INFIELD_X_CHART["g3"]["rp"]}\n' \ + f'**G2**: {INFIELD_X_CHART["g2"]["rp"]}\n' \ + f'**G1**: {INFIELD_X_CHART["g1"]["rp"]}\n' \ + f'**SI1**: {INFIELD_X_CHART["si1"]["rp"]}\n' + elif error_dice == 4: + error_note = '1-base error for e15, e16, e40' + elif error_dice == 3: + error_note = '2-base error for e8 -> e12, e26 -> e28, e39 -> e43\n' \ + '1-base error for e6, e7, e17, e30, e33, e44' + + roll_message = f'```md\n'\ + f'# {str(d_twenty)},{str(d_six_one + d_six_two + d_six_three)}\n'\ + f'Details:[1d20;3d6 ({d_twenty} - {d_six_one} {d_six_two} {d_six_three})]```' + + if range_note and error_note: + embed = await self.get_dice_embed(ctx.channel, None, None) + embed.title = f'{args[0][:2].upper()} Fielding Check Summary' + embed.add_field(name='Range Result', value=range_note, inline=False) + embed.add_field(name='Error Result', value=error_note, inline=False) + embed.add_field(name='Help Commands', + value=f'Run ! for full chart readout (e.g. `!g1` or `!do3`)') + embed.add_field(name='Range Chart', value=x_chart, inline=False) + embed.add_field(name='Error Chart', value=error_chart, inline=False) + embed.add_field(name='Result Reference', value=symbol_link, inline=False) + + drama_roll = random.randint(1, 20) + sixes = d_six_one + d_six_two + d_six_three + if (d_twenty < 6 and drama_roll > 10) or (sixes > 14 and drama_roll > 10): + await ctx.channel.send( + f'Ope, this looks like a tough play. I wonder if it\'s going to slip by the defender ' + f'{await get_emoji(ctx, "realeyes")}' + ) + await asyncio.sleep(2) + + await ctx.channel.send( + content=None, + embed=await self.get_dice_embed( + ctx.channel, + f'SA Fielding roll for {ctx.author.name}', + roll_message + ) + ) + await ctx.send(content=None, embed=embed) + if error_note1: + await ctx.send(content=error_note1) + if error_note2: + await ctx.send(content=error_note2) + else: + await ctx.channel.send( + content=None, + embed=await self.get_dice_embed( + ctx.channel, + f'SA Fielding roll for {ctx.author.name}', + roll_message + ) + ) + + this_roll = { + 'season': self.current['season'], + 'week': self.current['week'], + 'team_id': team["id"] if team else None, + 'roller': ctx.author.id, + 'threedsix': d_six_one + d_six_two + d_six_three, + 'dtwenty': d_twenty + } + self.rolls.append(this_roll) + + @commands.command(name='s', aliases=['shift']) + async def shift_roll(self, ctx): + """ + Make an infield shift roll. + """ + team = None + d_six_one = random.randint(1, 6) + d_twenty = random.randint(1, 20) + d_twenty_two = random.randint(1, 20) + embed = None + + if d_twenty < 3: + roll_message = f'```md\nHit into shift, consult chart```\n' \ + f'At bat roll for {ctx.author.name}\n```md\n# {d_six_one},{d_twenty_two}' \ + f'\nDetails:[1d6;1d20 ({d_six_one} - {d_twenty_two})]```' + embed = await self.get_dice_embed(ctx.channel, None, None) + embed.title = 'Hit Into Shift Results' + embed.set_image(url='https://lh3.googleusercontent.com/rouutaPsSd58B7XhGMc_uyo1CIP6I9QGRLnD0ZilizVoyaenKA' + 'GxHETVfsNisQ3rfxbIRYcRuiBxpMZVG-7lH0TvDHgipV6UW2XH7eWhDS4egEAXOyrjuOgqZVi72Uc2TItcIo' + 'hV8y5neRrck_N6juJzIsSm2YlZz5Pqyk_jRckK2qqyI5t15uBpoY9XbjmvgkJDNyaKBQYh7TgHd42BMzanYo' + 'Gr0gmCgLpiRcYxA3qEpSnxEzL7XGYODczTj6kiPOknFc6vQtcLoZ75wYqqU1AG9u5URKOckIworz0uJxrFaR' + 'PwewW5Fnxj36tf6bO37zl-RuH-veLBADtFmdEUT0pGt8eCmoB7YD6WtFG048Ox0n7U_QI3xIsvpMsnIVjml_' + 'sKQ55cHqsMZbnJLFXIO73Fl4xQFS1eqI-FjzwvK_kmslPznxfb0uz-WtBQ3fTUV--07ya-b_n4O0H38IVKKa' + 'eQqKzdTjU6Uv0uV8375UsNdSif5cbsfBp7_qlrZx4zFWc-IafCNh3h5R_NqLBV_-VQVqQdyu15Mbjr3s7kwB' + 'DANYsk9zv3grE1yoKzzAPtRILXxPpJz6MkXDPNSTDPOd6ZXR7uoyevt5BvvcWnf7Htgai8WtKHp3Zcd_bIGJ' + 'sGhohp4g3JO8zK5WMKN3-Za-KxH9XxDwrik-dc05x5VeX99m2A9eKClNbQGdE4TaI=w632-h292-no?authu' + 'ser=0') + else: + roll_message = f'```md\n' + if d_twenty < 9 and d_six_one > 3: + roll_message += f'Swing away, HR becomes 2B if beating the shift```\n' + else: + roll_message += f'Swing away, no effect```\n' + + await ctx.channel.send( + content=None, + embed=await self.get_dice_embed( + ctx.channel, + f'Shift roll for {ctx.author.name}', + roll_message + ) + ) + + this_roll = { + 'season': self.current['season'], + 'week': self.current['week'], + 'team_id': team["id"] if team else None, + 'roller': ctx.author.id, + 'dsix': d_six_one, + 'dtwenty': d_twenty + } + self.rolls.append(this_roll) + this_roll = { + 'season': self.current['season'], + 'week': self.current['week'], + 'team_id': team["id"] if team else None, + 'roller': ctx.author.id, + 'dtwenty': d_twenty_two + } + self.rolls.append(this_roll) + + @commands.command( + name='lookups', help='Fielding chart lookup', + aliases=['si1', 'si2', 'do2', 'do3', 'tr3', 'f1', 'f2', 'f3', 'po', 'wp', 'x', 'fo', 'g1', 'g2', 'g3']) + async def fielding_chart_x(self, ctx): + this_command = ctx.message.content.split(" ")[0][1:] + chart_data = get_xchart_data(this_command) + if not chart_data: + await ctx.send('Ope. Something went wrong pulling that info. Sorry about that.') + return + + await ctx.send(chart_data) + + if this_command in ['g1', 'g2', 'g3']: + embeds = get_groundball_embeds(this_command) + await ctx.send(content=None, embed=embeds[0]) + + @commands.command(name='groundball', aliases=['gba', 'gbb', 'gbc'], help='Groundball charts') + async def groundball_chart(self, ctx): + this_command = ctx.message.content.split(" ")[0][1:] + embeds = get_groundball_embeds(this_command) + await ctx.send(content=None, embed=embeds[0]) + await ctx.send(content=None, embed=embeds[1]) + + @commands.command(name='roll', aliases=['r'], help='roll XdY') + async def roll_command(self, ctx, roll_string): + if not re.search('[0-9]+d[0-9]+', roll_string.lower()): + await ctx.send('Please format the dice as **X**d**Y** and try again.') + return + + numbers = re.split('d', roll_string.lower()) + logging.info(f'numbers: {numbers}') + num_dice = int(numbers[0]) + die_sides = int(numbers[1]) + all_rolls = [] + total = 0 + + logging.info(f'num_dice: {num_dice} / die_sides: {die_sides}') + + if num_dice < 1 or die_sides < 1: + await ctx.send('Hurr hurr, let\'s roll negative dice, amirite!') + return + + if num_dice * die_sides > 100000: + await ctx.send('Ain\'t nobody got time for dat!') + return + + for x in range(num_dice): + this_roll = random.randint(1, die_sides) + total += this_roll + all_rolls.append(f'{this_roll}') + + roll_message = f'```md\n' \ + f'# {total}\n' \ + f'Details: [{num_dice}d{die_sides} ({" ".join(all_rolls)})]\n```' + + await ctx.channel.send( + content=None, + embed=await self.get_dice_embed( + ctx.channel, + f'{num_dice}d{die_sides} roll for {ctx.author.name}', + roll_message + ) + ) + + +async def setup(bot): + await bot.add_cog(Dice(bot)) diff --git a/cogs/draft.py b/cogs/draft.py new file mode 100644 index 0000000..6a147d9 --- /dev/null +++ b/cogs/draft.py @@ -0,0 +1,1075 @@ +import math +import re + +from helpers import * +from db_calls import * +from discord.ext import commands, tasks + +from discord import TextChannel, app_commands + + +class Draft(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.warnings = 0 + + self.draft_loop.start() + + async def cog_command_error(self, ctx, error): + await ctx.send(f'{error}\n\nRun !help to see the command requirements') + + async def slash_error(self, ctx, error): + await ctx.send(f'{error}') + + @tasks.loop(seconds=10) + async def draft_loop(self): + # guild = self.bot.get_guild(int(os.environ.get('GUILD_ID'))) + # if not guild: + # logging.error('Bot not logged in - delaying draft loop') + # return + + draft_data = await get_draftdata() + now = datetime.datetime.now() + # logging.info('Entering draft loop') + try: + deadline = datetime.datetime.strptime(draft_data['pick_deadline'], '%Y-%m-%dT%H:%M:%S.%f') + except Exception as e: + deadline = None + + # logging.info(f'Timer: {draft_data["timer"]} / Deadline: {deadline} / Warnings: {self.warnings}') + # logging.info(f'10 Hrs?: {deadline - datetime.timedelta(hours=10) <= now}') + + # Timer is active and pick is due + if draft_data['timer'] and deadline: + current = await get_current() + draft_pick = await get_one_draftpick_byoverall(current['season'], draft_data['currentpick']) + team_role = get_team_role(None, draft_pick['owner'], self.bot) + + if deadline <= now: + # Auto-draft + team_list = await get_draft_list(draft_pick['owner']) + if len(team_list) > 0: + for x in team_list.values(): + this_pick = await self.draft_player(current, draft_data, draft_pick, x['player']) + if this_pick['success']: + break + + await self.advance_pick() + + else: + # # Slow Draft + # if (deadline - datetime.timedelta(hours=10) <= now) and self.warnings == 0: + # current = await get_current() + # draft_pick = await get_one_draftpick_byoverall(current['season'], draft_data['currentpick']) + # team_role = get_team_role(None, draft_pick['owner'], self.bot) + # await send_to_channel( + # self.bot, + # draft_data['ping_channel'], + # f'{team_role.mention if team_role else draft_pick["owner"]["lname"]}\n' + # f'You are two hours in on pick #{draft_pick["overall"]}.' + # ) + # self.warnings = 1 + # elif (deadline - datetime.timedelta(hours=6) <= now) and self.warnings == 1: + # current = await get_current() + # draft_pick = await get_one_draftpick_byoverall(current['season'], draft_data['currentpick']) + # team_role = get_team_role(None, draft_pick['owner'], self.bot) + # await send_to_channel( + # self.bot, + # draft_data['ping_channel'], + # f'{team_role.mention if team_role else draft_pick["owner"]["lname"]}\n' + # f'You are halfway into the 12-hour timer for pick #{draft_pick["overall"]}.' + # ) + # self.warnings = 2 + # elif (deadline - datetime.timedelta(hours=2) <= now) and self.warnings == 2: + # current = await get_current() + # draft_pick = await get_one_draftpick_byoverall(current['season'], draft_data['currentpick']) + # team_role = get_team_role(None, draft_pick['owner'], self.bot) + # await send_to_channel( + # self.bot, + # draft_data['ping_channel'], + # f'{team_role.mention if team_role else draft_pick["owner"]["lname"]}\n' + # f'You have two hours remaining to make pick #{draft_pick["overall"]}.' + # ) + # self.warnings = 3 + # elif (deadline - datetime.timedelta(hours=1) <= now) and self.warnings == 3: + # current = await get_current() + # draft_pick = await get_one_draftpick_byoverall(current['season'], draft_data['currentpick']) + # team_role = get_team_role(None, draft_pick['owner'], self.bot) + # await send_to_channel( + # self.bot, + # draft_data['ping_channel'], + # f'{team_role.mention if team_role else draft_pick["owner"]["lname"]}\n' + # f'You have one hour remaining to make pick #{draft_pick["overall"]}.' + # ) + # self.warnings = 4 + # elif (deadline - datetime.timedelta(minutes=5) <= now) and self.warnings == 4: + # current = await get_current() + # draft_pick = await get_one_draftpick_byoverall(current['season'], draft_data['currentpick']) + # team_role = get_team_role(None, draft_pick['owner'], self.bot) + # await send_to_channel( + # self.bot, + # draft_data['ping_channel'], + # f'{team_role.mention if team_role else draft_pick["owner"]["lname"]}\n' + # f'You have five minutes remaining to make pick #{draft_pick["overall"]}. Is Neil Walker available?' + # ) + # self.warnings = 5 + + # 2-minute draft + if (deadline - datetime.timedelta(seconds=59) <= now) and self.warnings < 1: + await send_to_channel( + self.bot, + draft_data['ping_channel'], + f'{team_role.mention if team_role else draft_pick["owner"]["lname"]}\n' + f'Less than a minute remaining to make pick #{draft_pick["overall"]}!' + ) + self.warnings += 1 + elif (deadline - datetime.timedelta(seconds=29) <= now) and self.warnings < 2: + await send_to_channel( + self.bot, + draft_data['ping_channel'], + f'{team_role.mention if team_role else draft_pick["owner"]["lname"]}\n' + f'Less than 30 seconds remaining to make pick #{draft_pick["overall"]}!' + ) + self.warnings += 1 + + async def update_timer(self, draft_data): + deadline = datetime.datetime.now() + datetime.timedelta(minutes=draft_data['pick_minutes']) + await patch_draftdata(pick_deadline=deadline) + + async def send_draft_ping(self, ping=True): + current = await get_current() + draft_data = await get_draftdata() + logging.info(f'current: {current}\nd_data: {draft_data}') + this_pick = await get_one_draftpick_byoverall(current['season'], draft_data['currentpick']) + logging.info(f'pick: {this_pick}') + this_team = await get_one_team(this_pick['owner']['id']) + logging.info(f'team: {this_team}') + team_role = get_team_role(None, this_team, self.bot) + logging.info(f'role: {team_role}') + team_ping = None + if ping and team_role: + team_ping = team_role.mention + pick_num = this_pick['overall'] % 16 + if pick_num == 0: + pick_num = 16 + + logging.info(f'current: {current}\ndata: {draft_data}\npick: {this_pick}\nteam: {this_team}\n' + f'role: {team_role}\nping: {team_ping}') + + embed = get_team_embed(f'{this_team["lname"]} On The Clock', team=this_team) + embed.description = f'Round {this_pick["round"]} / Pick {pick_num} / Overall {this_pick["overall"]}' + + players = await get_players(current['season'], team_abbrev=this_team['abbrev'], sort='wara-desc') + if players: + count = 0 + core_players_string = '' + + for x in players: + if count < 5: + core_players_string += f'{players[x]["pos_1"]} {players[x]["name"]} ({players[x]["wara"]})\n' + count += 1 + + embed.add_field(name='Core Players', value=core_players_string) + + roster = await get_team_roster(this_team, 'current') + embed.add_field(name=f'{this_team["sname"]} sWAR', value=f'{roster["active"]["WARa"]:.2f}') + + embed.add_field( + name=f'{this_team["abbrev"]} Roster Page', + value=f'https://sombaseball.ddns.net/teams?abbrev={this_team["abbrev"]}', + inline=False + ) + + embed.add_field( + name=f'Draft Sheet', + value=f'https://docs.google.com/spreadsheets/d/1BgySsUlQf9K21_uOjQOY7O0GrRfF6zt1BBaEFlvBokY/' + f'edit#gid=937613012', + inline=False + ) + + # Last 5 Loop + all_picks = await get_draftpicks( + current['season'], overall_start=this_pick['overall'] - 15, overall_end=this_pick['overall'] - 1 + ) + last_five = dict(sorted(all_picks.items(), key=lambda item: item[1]["overall"], reverse=True)) + last_string = '' + count = 0 + for x in last_five: + if last_five[x]['player']: + last_string += f'Pick #{last_five[x]["overall"]}: {last_five[x]["player"]["name"]}\n' + count += 1 + if count >= 5: + break + if len(last_string) == 0: + last_string = 'None, yet' + embed.add_field(name='Last 5', value=last_string) + + # Next 5 Loop + all_picks = await get_draftpicks( + current['season'], overall_start=this_pick['overall'] + 1, overall_end=this_pick['overall'] + 15 + ) + next_five = dict(sorted(all_picks.items(), key=lambda item: item[1]["overall"])) + next_string = '' + count = 0 + for x in next_five: + if not next_five[x]['player']: + next_string += f'Pick #{next_five[x]["overall"]}: {next_five[x]["owner"]["sname"]}\n' + count += 1 + if count >= 5: + break + if len(next_string) == 0: + next_string = 'None, yet' + embed.add_field(name='Next 5', value=next_string) + + # Pick Deadline + if draft_data['pick_deadline']: + deadline = datetime.datetime.strptime(draft_data['pick_deadline'], '%Y-%m-%dT%H:%M:%S.%f') + deadline = deadline - datetime.timedelta(hours=6) + dead_string = deadline.strftime("%b %d @ %H:%M Central") + else: + dead_string = 'None' + embed.add_field(name='Pick Deadline', value=dead_string, inline=False) + + await send_to_channel(self.bot, draft_data['ping_channel'], content=team_ping, embed=embed) + + async def advance_pick(self): + current = await get_current() + draft_data = await get_draftdata() + draft_pick = await get_one_draftpick_byoverall(current['season'], draft_data['currentpick']) + + if not draft_pick['player']: + await send_to_channel( + self.bot, + draft_data['ping_channel'], + f'It\'s time to to ***SKIP***. Can I get an F in chat?' + ) + self.warnings = 0 + await patch_draftdata(currentpick=draft_data['currentpick'] + 1) + + # Advance the current pick until a selection is possible + while True: + draft_data = await get_draftdata() + draft_pick = await get_one_draftpick_byoverall(current['season'], draft_data['currentpick']) + + # Check that selection has been made for current pick, advance if so + if not draft_pick: + await send_to_channel(self.bot, draft_data['ping_channel'], 'Looks like that is the end of the draft!') + await patch_draftdata(timer=False) + return + elif draft_pick['player']: + await patch_draftdata(currentpick=draft_data['currentpick'] + 1) + else: + break + + # If timer is true, set new deadline + draft_data = await get_draftdata() + if draft_data['timer']: + await self.update_timer(draft_data) + else: + deadline = datetime.datetime.now() + datetime.timedelta(days=690) + await patch_draftdata(pick_deadline=deadline) + + # Post splash screen to ping_channel + await self.send_draft_ping() + + async def send_pick_to_sheets(self, draft_pick, player, pick_num): + sheets = pygsheets.authorize(service_file='storage/major-domo-service-creds.json') + + # this_pick = [[ + # draft_pick['round'], pick_num, draft_pick['owner']['abbrev'], draft_pick['owner']['sname'], + # player['name'], player['wara'], draft_pick['overall'] + # ]] + this_pick = [[ + draft_pick['origowner']['abbrev'], draft_pick['owner']['abbrev'], player['name'], + player['wara'] + ]] + + sheets.open_by_key(SBA_SEASON7_DRAFT_KEY).worksheet_by_title('Ordered List').update_values( + crange=f'D{draft_pick["overall"] + 1}', + values=this_pick + ) + + async def draft_player(self, current, draft_data, draft_pick, player): + # Is this player available to be drafted? + if player['team'] == draft_pick['owner']: + return {'success': False, 'error': f'{player["name"]} is already on your team, dingus.'} + elif player['team']['abbrev'] != 'FA': + return { + 'success': False, + 'error': f'Hey, uh, {player["team"]["sname"]}, you think {draft_pick["owner"]["abbrev"]} can have ' + f'{player["name"]} for free? No? Sorry, I tried.' + } + + team_roster = await get_team_roster(draft_pick['owner'], 'current') + + # Does this team have the cap space to afford this player? + max_zeroes = 32 - len(team_roster['active']['players']) + max_counted = 26 - max_zeroes + total_swar = 0 + count = 0 + + for x in reversed(team_roster['active']['players']): + count += 1 + if count > max_counted: + break + + total_swar += x['wara'] + + if total_swar > 38.00001: + return { + 'success': False, + 'error': f'Drafting {player["name"]} would put you at {total_swar:.2f} ' + f'sWAR, friendo.' + } + + # total_wara = team_roster["active"]["WARa"] + player["wara"] + # if total_wara > 45.00001: + # return { + # 'success': False, + # 'error': f'Drafting {player["name"]} would put you at {total_wara:.2f} ' + # f'WARa, friendo.' + # } + + # Submit the pick + logging.info( + f'{draft_pick["owner"]["lname"]} selects {player["name"]} with the #{draft_pick["overall"]} overall pick' + ) + await patch_draftpick(draft_pick['id'], player_id=player['id']) + await patch_player(player['id'], team_id=draft_pick['owner']['id'], demotion_week=2) + await post_transactions([{ + 'week': -1, + 'player_id': player['id'], + 'oldteam_id': 201, # FA team ID + 'newteam_id': draft_pick['owner']['id'], + 'season': current['season'], + 'moveid': f'draft-overall-{draft_pick["overall"]}' + }]) + await send_to_channel( + self.bot, + draft_data['ping_channel'], + embed=await get_player_embed(await get_one_player(player['id']), current) + ) + if player['image2']: + embed = get_team_embed(f'{player["name"]}', player["team"], thumbnail=False) + embed.set_image(url=player['image2']) + await send_to_channel( + self.bot, + draft_data['ping_channel'], + embed=embed + ) + + pick_num = draft_pick['overall'] % 16 + if pick_num == 0: + pick_num = 16 + + await self.send_pick_to_sheets(draft_pick, player, draft_pick['overall']) + + await send_to_channel( + self.bot, + draft_data['result_channel'], + f'Round {draft_pick["round"]} / Pick {pick_num}: ' + f'{draft_pick["owner"]["abbrev"]} selects **{player["name"]}**' + ) + return {'success': True} + + async def draftdata_to_string(self, current, draft_data): + try: + draft_pick = await get_one_draftpick_byoverall(current['season'], draft_data['currentpick']) + except Exception as e: + logging.error(f'draft cog - Could not get current draft pick: {e}') + draft_pick = None + if draft_data['pick_deadline']: + deadline = datetime.datetime.strptime(draft_data['pick_deadline'], '%Y-%m-%dT%H:%M:%S.%f') + deadline = deadline - datetime.timedelta(hours=6) + dead_string = deadline.strftime("%b %d @ %H:%M Central") + else: + dead_string = 'None' + + guild = self.bot.get_guild(int(os.environ.get('GUILD_ID'))) + draft_string = f'Current Pick: {draft_data["currentpick"]}\n' \ + f'Pick Owner: {draft_pick["owner"]["sname"] if draft_pick else "N/A"}\n' \ + f'Timer: {"Active" if draft_data["timer"] else "Inactive"} ' \ + f'({draft_data["pick_minutes"]} min total)\n' \ + f'Pick Deadline: {dead_string}\n' \ + f'Ping Channel: ' \ + f'{discord.utils.get(guild.text_channels, id=draft_data["ping_channel"]).mention}\n' \ + f'Result Channel: ' \ + f'{discord.utils.get(guild.text_channels, id=draft_data["result_channel"]).mention}\n' + + return draft_string + + # @commands.command(name='start', help='Start pick timer') + # @commands.is_owner() + # async def advance_draft_command(self, ctx): + # current = await get_current() + # draft_data = await get_draftdata() + # draft_pick = await get_one_draftpick_byoverall(current['season'], draft_data['currentpick']) + # + # if draft_pick['player']: + # await self.advance_pick() + # else: + # if draft_data['timer']: + # deadline = datetime.datetime.now() + datetime.timedelta(minutes=draft_data['pick_minutes']) + # else: + # deadline = datetime.datetime.now() + datetime.timedelta(days=690) + # await patch_draftdata(pick_deadline=deadline) + # + # self.warnings = 0 + # await self.send_draft_ping() + + @commands.command(name='select', aliases=['pick', 'draft'], help='Draft a player') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME) + async def draft_command(self, ctx, *, name): + cal_can_pick_for_all = True + current = await get_current() + team = await get_team_by_owner(current['season'], ctx.author.id) + if not team: + await ctx.message.add_reaction('❌') + await ctx.send('I don\'t know youuuuuuuuu') + return + + draft_data = await get_draftdata() + draft_pick = await get_one_draftpick_byoverall(current['season'], draft_data['currentpick']) + alt_pick_flag = False + + # Does the current pick belong to this team? + if draft_pick['owner'] != team: + alt_pick_flag = True + # Does this team have any skipped picks? + raw_picks = await get_draftpicks( + current['season'], owner_team=team, round_end=math.ceil(draft_pick['overall'] / 16), round_start=1 + ) + team_picks = dict(sorted(raw_picks.items(), key=lambda item: item[1]["overall"])) + new_pick = None + + for x in team_picks: + if not team_picks[x]["player"] and team_picks[x]['overall'] < draft_pick['overall']: + new_pick = await get_one_draftpick_byoverall(current['season'], team_picks[x]['overall']) + break + + if new_pick: + draft_pick = new_pick + else: + mil_team = await get_one_team(f'{team["abbrev"]}MiL') + if mil_team == draft_pick['owner']: + team = mil_team + elif ctx.author.id == self.bot.owner_id and cal_can_pick_for_all: + alt_pick_flag = False + else: + await ctx.message.add_reaction('❌') + await ctx.send(f'You are not on the clock {ctx.author.mention}. **{draft_pick["owner"]["abbrev"]}** is ' + f'up right now.') + return + + player_cog = self.bot.get_cog('Players') + player_name = await fuzzy_player_search(ctx, ctx.channel, self.bot, name, player_cog.player_list.keys()) + player = await get_one_player(player_name) + + the_pick = await self.draft_player(current, draft_data, draft_pick, player) + if the_pick['success']: + await ctx.message.add_reaction('✅') + if not alt_pick_flag: + await self.advance_pick() + else: + await react_and_reply(ctx, '❌', the_pick['error']) + + @commands.command(name='list', aliases=['draftlist', 'mylist'], help='Set your draft list') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME) + async def draft_list_command(self, ctx, *player_list): + current = await get_current() + team = await get_team_by_owner(current['season'], ctx.author.id) + if not team: + await react_and_reply(ctx, '❌', 'I don\'t know youuuuuuuuu') + return + + if not player_list: + team_list = await get_draft_list(team) + if len(team_list) > 0: + list_string = 'Current list:\n' + for x in team_list.values(): + list_string += f'{x["rank"]}. {x["player"]["name"]}\n' + await ctx.send(list_string) + else: + await ctx.send('I do not have a list for you.') + return + else: + player_list = ' '.join(player_list) + + player_names = re.split(',', player_list) + draft_list = [] + spelling = [] + errors = [] + rank = 0 + + for x in player_names: + rank += 1 + player_cog = self.bot.get_cog('Players') + try: + player_name = await fuzzy_player_search(ctx, ctx.channel, self.bot, x, player_cog.player_list.keys()) + player = await get_one_player(player_name) + except Exception as e: + logging.error(f'Could not draft {x} for {team["abbrev"]}: {e}') + errors.append(x) + rank -= 1 + else: + if player['team']['abbrev'] != 'FA': + errors.append(player['name']) + rank -= 1 + else: + draft_list.append({ + 'season': current['season'], + 'team_id': team['id'], + 'rank': rank, + 'player_id': player['id'] + }) + + if len(errors) > 0: + error_string = 'The following players have already been drafted: ' + error_string += ', '.join(errors) + await react_and_reply(ctx, '😬', error_string) + + if len(spelling) > 0: + error_string = 'Who the fuck is this: ' + error_string += ', '.join(spelling) + await react_and_reply(ctx, '❓', error_string) + + await post_draft_list(draft_list) + await ctx.message.add_reaction('✅') + + @commands.command(name='whomst', aliases=['draftstatus'], help='Get draft status') + async def draft_status_command(self, ctx): + current = await get_current() + draft_data = await get_draftdata() + + await ctx.send(await self.draftdata_to_string(current, draft_data)) + if draft_data["ping_channel"]: + try: + await self.send_draft_ping(ping=False) + except Exception as e: + logging.error(f'!whomst - Could not post current draft ping: {e}') + + @app_commands.command(name='draft-mod', description='Mod commands for draft administration') + @app_commands.checks.has_any_role('Da Commish') + @app_commands.describe( + result_channel='Text Channel: where Major Domo posts draft results', + ping_channel='Text Channel: where Major Domo pings for draft picks', + current_overall='Integer: override the current pick number', + timer_master='Integer: number of minutes for all draft picks', + timer_this_pick='Integer: number of minutes for the current draft pick', + timer_active='Boolean: enable/disable the pick timer', + wipe_pick='Integer: overall pick number to delete; cannot be combined with other parameters' + ) + async def draftmod_slash_command( + self, interaction: discord.Interaction, result_channel: TextChannel = None, + ping_channel: TextChannel = None, current_overall: int = None, timer_master: int = None, + timer_this_pick: int = None, timer_active: bool = None, wipe_pick: int = None): + await interaction.response.defer() + current = await get_current() + draft_data = await get_draftdata() + + pick_deadline = None + res_channel_id = None + ping_channel_id = None + + if wipe_pick is not None: + this_pick = await get_one_draftpick_byoverall(current['season'], overall=wipe_pick) + if not this_pick['player']: + await interaction.edit_original_response(content=f'I don\'t see a player taken {wipe_pick} overall.') + else: + fa_team = await get_one_team('FA') + await patch_draftpick( + pick_id=this_pick['id'], + player_id=False + ) + await patch_player( + pid=this_pick['player']['id'], + team_id=fa_team['id'] + ) + this_player = await get_one_player(this_pick['player']['id']) + await interaction.edit_original_response( + content='Don\'t forget to delete the transaction from the database.', + embed=await get_player_embed(this_player, current) + ) + return + + if timer_this_pick is not None: + pick_deadline = datetime.datetime.now() + datetime.timedelta(minutes=timer_this_pick) + if timer_active is True: + if timer_this_pick is not None: + timer = timer_this_pick + elif timer_master is not None: + timer = timer_master + else: + timer = draft_data['pick_minutes'] + pick_deadline = datetime.datetime.now() + datetime.timedelta(minutes=timer) + if result_channel is not None: + res_channel_id = result_channel.id + if ping_channel is not None: + ping_channel_id = ping_channel.id + if current_overall is not None and draft_data['timer'] and timer_active is None: + pick_deadline = datetime.datetime.now() + datetime.timedelta(minutes=draft_data['pick_minutes']) + + await patch_draftdata( + currentpick=current_overall, + timer=timer_active, + pick_deadline=pick_deadline, + result_channel=res_channel_id, + ping_channel=ping_channel_id, + pick_minutes=timer_master + ) + + draft_data = await get_draftdata() + await interaction.edit_original_response(content=await self.draftdata_to_string(current, draft_data)) + + if timer_active is True or draft_data['timer']: + self.warnings = 0 + draft_pick = await get_one_draftpick_byoverall(current['season'], draft_data['currentpick']) + if draft_pick['player']: + await self.advance_pick() + else: + await self.send_draft_ping() + + # @commands.group(name='draftset', help='Mod commands for running the draft') + # @commands.is_owner() + # async def draft_set_group(self, ctx): + # if ctx.invoked_subcommand is None: + # await ctx.send('draftset what? You forgot the rest of the command.') + # + # @draft_set_group.command(name='results', help='Set result channel') + # @commands.is_owner() + # async def draft_set_results_command(self, ctx, *channel_mention: TextChannel): + # if not channel_mention: + # this_channel = ctx.channel + # else: + # this_channel = channel_mention[0] + # if not this_channel: + # await ctx.send(f'I cannot find **{channel_mention[0]}** - is that spelled correctly?') + # return + # + # draft_data = await get_draftdata() + # current_channel = discord.utils.get(ctx.guild.text_channels, id=draft_data["result_channel"]) + # prompt = f'The result channel is currently **{current_channel}** - would you like to set it to ' \ + # f'**{this_channel.mention}**?' + # + # this_q = Question(self.bot, ctx.channel, prompt, 'yesno', 15) + # resp = await this_q.ask([ctx.author]) + # + # if not resp: + # await ctx.send('I will leave it be for now.') + # return + # else: + # await patch_draftdata(result_channel=this_channel.id) + # await ctx.send(random_conf_gif()) + # + # @draft_set_group.command(name='ping', help='Set ping channel') + # @commands.is_owner() + # async def draft_set_ping_command(self, ctx, *channel_mention: TextChannel): + # if not channel_mention: + # this_channel = ctx.channel + # else: + # this_channel = channel_mention[0] + # if not this_channel: + # await ctx.send(f'I cannot find **{channel_mention[0]}** - is that spelled correctly?') + # return + # + # draft_data = await get_draftdata() + # current_channel = discord.utils.get(ctx.guild.text_channels, id=draft_data["ping_channel"]) + # prompt = f'The result channel is currently **{current_channel}** - would you like to set it to ' \ + # f'**{this_channel.mention}**?' + # + # this_q = Question(self.bot, ctx.channel, prompt, 'yesno', 15) + # resp = await this_q.ask([ctx.author]) + # + # if not resp: + # await ctx.send('I will leave it be for now.') + # return + # else: + # await patch_draftdata(ping_channel=this_channel.id) + # await ctx.send(random_conf_gif()) + # + # @draft_set_group.command(name='overall', help='Set current pick') + # @commands.is_owner() + # async def draft_set_overall_command(self, ctx, overall_num: int): + # draft_data = await get_draftdata() + # prompt = f'The current pick is #**{draft_data["currentpick"]}** - would you like to set it to ' \ + # f'#**{overall_num}**?' + # + # this_q = Question(self.bot, ctx.channel, prompt, 'yesno', 15) + # resp = await this_q.ask([ctx.author]) + # + # if not resp: + # await ctx.send('I will leave it be for now.') + # return + # else: + # await patch_draftdata(currentpick=overall_num) + # await ctx.send(random_conf_gif()) + # + # @draft_set_group.command(name='minutes', help='Set minutes per pick') + # @commands.is_owner() + # async def draft_set_minutes_command(self, ctx, minutes: int): + # draft_data = await get_draftdata() + # prompt = f'The current pick time is **{draft_data["pick_minutes"]}** minutes - would you like to set it to ' \ + # f'**{minutes}**?' + # + # this_q = Question(self.bot, ctx.channel, prompt, 'yesno', 15) + # resp = await this_q.ask([ctx.author]) + # + # if not resp: + # await ctx.send('I will leave it be for now.') + # return + # else: + # await patch_draftdata(pick_minutes=minutes) + # await self.update_timer(draft_data) + # await ctx.send(random_conf_gif()) + # + # @draft_set_group.command(name='timer', help='Start/stop the timer') + # @commands.is_owner() + # async def timer_start_command(self, ctx, which): + # if which.lower() in ['start', 'go', 'begin', 'on', 'yes']: + # await patch_draftdata(timer=True) + # await ctx.message.add_reaction('✅') + # await ctx.send(f'Just enabled the timer {await get_emoji(ctx, "fingerguns")}') + # else: + # await patch_draftdata(timer=False) + # await ctx.message.add_reaction('✅') + # await ctx.send(f'Just stopped the timer {await get_emoji(ctx, "fingerguns")}') + + # @commands.command(name='keeper', help='Set keeper slots') + # @commands.is_owner() + # async def keeper_command(self, ctx, team_abbrev, draft_round, *, name): + # current = await get_current() + # this_team = await get_one_team(team_abbrev) + # if not this_team: + # await ctx.send(f'Are you familiar with this league? Who the fuck is **{team_abbrev}**?') + # return + # + # this_pick = await get_one_draftpick_search(current['season'], this_team['abbrev'], draft_round) + # if not this_pick: + # await ctx.send(f'{await get_emoji(ctx, "grimacing")}Uhh...I couldn\'t find ' + # f'{this_team["abbrev"]} {draft_round}.') + # return + # + # player_cog = self.bot.get_cog('Players') + # player_name = await fuzzy_player_search(ctx, ctx.channel, self.bot, name, player_cog.player_list.keys()) + # this_player = await get_one_player(player_name) + # + # await patch_draftpick(this_pick['id'], player_id=this_player['id']) + # await ctx.send(content=None, embed=await get_player_embed(this_player, current)) + # + # pick_num = this_pick['overall'] % 18 + # if pick_num == 0: + # pick_num = 18 + # + # await self.send_pick_to_sheets(this_pick, this_player, pick_num) + # + # @commands.command(name='cutem', help='Release non-keepers') + # @commands.is_owner() + # async def cutem_command(self, ctx, team_abbrev): + # current = await get_current() + # if current['week'] == -2: + # await ctx.send('Advance the week and run transactions before running this.') + # return + # this_team = await get_one_team(team_abbrev) + # if not this_team: + # await ctx.send(f'Are you familiar with this league? Who the fuck is **{team_abbrev}**?') + # return + # + # keepers = [] + # + # all_picks = await get_draftpicks(current['season'], owner_team=this_team) + # for x in all_picks: + # if all_picks[x]['player']: + # keepers.append(all_picks[x]['player']) + # + # full_team = await get_players(current['season'], this_team['abbrev']) + # for x in full_team: + # if full_team[x] not in keepers: + # await patch_player(full_team[x]['id'], team_id=99) + # + # await ctx.send(random_conf_gif()) + + # @commands.command(name='migrateteam', help='Migrate s3 to s4', hidden=True) + # @commands.is_owner() + # async def migrate_teams_command(self, ctx, old_abbrev): + # # Create new team and clean out unused fields + # old_team = await get_one_team(old_abbrev, 4) + # new_team = copy.deepcopy(old_team) + # new_team['season'] = 5 + # del new_team['manager_legacy'] + # del new_team['division_legacy'] + # new_team['manager1_id'] = copy.deepcopy(new_team['manager1']['id']) + # del new_team['manager1'] + # if old_team['manager2']: + # new_team['manager2_id'] = copy.deepcopy(new_team['manager2']['id']) + # del new_team['manager2'] + # del new_team['division'] + # new_team['stadium'] = SBA_WEATHER_CHARTS[old_team['abbrev']] + # # del new_team['gsheet'] + # + # # Post new team and show embed to confirm + # await ctx.send(f'Copying the {old_team["sname"]} into season 4...') + # new_team = await post_team(new_team) + # if new_team: + # embed = get_team_embed(f'Season 4 {new_team["sname"]}', new_team) + # await ctx.send(content=None, embed=embed) + # + # # Post IL and MiL teams + # il_team = new_team + # il_team['abbrev'] = f'{il_team["abbrev"]}IL' + # del il_team['stadium'] + # if not await post_team(il_team): + # await ctx.send('Yikes! I wasn\'t able to post the SIL team.') + # return + # await ctx.send(f'Added {il_team["abbrev"]}') + # + # mil_team = il_team + # mil_team['abbrev'] = f'{old_team["abbrev"]}MiL' + # if not await post_team(mil_team): + # await ctx.send('Yikes! I wasn\'t able to post the MiL team.') + # return + # await ctx.send(f'Added {mil_team["abbrev"]}') + # + # # Update draft picks to new team + # await ctx.send('Alright, lemme move over draft picks...') + # all_picks = await get_draftpicks(5, owner_team=old_team, team_season=3) + # for this_pick in [*all_picks.values()]: + # await patch_draftpick(this_pick['id'], owner_id=new_team['id']) + # + # all_picks = await get_draftpicks(5, orig_owner_team=old_team, team_season=3) + # for this_pick in [*all_picks.values()]: + # await patch_draftpick(this_pick['id'], orig_owner_id=new_team['id']) + # + # await ctx.send('All done!') + + # @commands.command(name='linkteams', help='Migrate s3 to s4', hidden=True) + # @commands.is_owner() + # async def link_teams_command(self, ctx, old_abbrev, new_abbrev): + # old_team = await get_one_team(old_abbrev, 4) + # new_team = await get_one_team(new_abbrev, 5) + # + # active_players = await get_players(4, old_abbrev) + # sil_players = await get_players(4, f'{old_abbrev}SIL') + # mil_players = await get_players(4, f'{old_abbrev}MiL') + # + # prompt = f'I found {len(active_players)} players on {old_abbrev} - want me to add them to {new_team["abbrev"]}?' + # this_q = Question(self.bot, ctx.channel, prompt, 'yesno', 15) + # resp = await this_q.ask([ctx.author]) + # + # if not resp: + # await ctx.send('No problem - this is a big decision.') + # return + # else: + # players_not_found = [] + # for name in active_players: + # new_player = await get_one_player(name) + # + # if not new_player: + # players_not_found.append(name) + # else: + # await patch_player(new_player['id'], team_id=new_team['id']) + # + # for name in sil_players: + # new_player = await get_one_player(name) + # + # if not new_player: + # players_not_found.append(name) + # else: + # await patch_player(new_player['id'], team_id=f'{new_team["id"] + 1}') + # + # for name in mil_players: + # new_player = await get_one_player(name) + # + # if not new_player: + # players_not_found.append(name) + # else: + # await patch_player(new_player['id'], team_id=f'{new_team["id"] + 2}') + # + # await ctx.send('All done!') + # + # @commands.command(name='setorder', help='Populate all overall picks', hidden=True) + # @commands.is_owner() + # async def set_order_command(self, ctx, old_abbrev, new_abbrev, overall_num: int): + # old_team = await get_one_team(old_abbrev, 4) + # this_team = await get_one_team(new_abbrev, 5) + # if not this_team: + # this_team = await get_one_team('FA', 5) + # + # all_picks = await get_draftpicks(5, orig_owner_team=this_team) + # count = 1 + # + # await ctx.send(f'Okay, let me set each of their 11 first rounds with multiples of {overall_num} before ' + # f'snaking...') + # for this_pick in [*all_picks.values()]: + # if count > 11 and count % 2 == 0: + # this_overall = ((count - 1) * 18) + (19 - overall_num) + # else: + # this_overall = (count * 18) - (18 - overall_num) + # await patch_draftpick( + # this_pick['id'], + # overall=this_overall, + # orig_owner_id=this_team['id'] + # ) + # count += 1 + # + # await ctx.send(random_conf_gif()) + # + # @commands.command(name='fixorder', help='Populate all overall picks', hidden=True) + # @commands.is_owner() + # async def set_order_command(self, ctx): + # count = 393 + # f_order = { + # "LMM": 1, + # "TK": 2, + # "WV": 3, + # "BSG": 4, + # "BBL": 5, + # "MAD": 6, + # "FA": 7, + # "MKE": 9, + # "VA": 10, + # "DEN": 11, + # "WWS": 12, + # "NN": 13, + # "HOL": 14, + # "CLS": 15, + # "DAL": 16, + # "KSS": 17, + # "PBW": 18 + # } + # + # for x in range(25, 36): + # raw_picks = await get_draftpicks(5, round_start=x, round_end=x) + # all_picks = dict(sorted(raw_picks.items(), key=lambda item: f_order[item[1]["origowner"]["abbrev"]])) + # for this_pick in [*all_picks.values()]: + # await patch_draftpick( + # this_pick['id'], + # overall=count, + # ) + # count += 1 + # + # await ctx.send(random_conf_gif()) + # + # @commands.command(name='newowner', help='Update draft owner seasons', hidden=True) + # @commands.is_owner() + # async def new_owner_command(self, ctx, old_abbrev, new_abbrev): + # old_team = await get_one_team(old_abbrev, 4) + # this_team = await get_one_team(new_abbrev, 5) + # if not this_team: + # this_team = await get_one_team('FA', 5) + # + # all_picks = await get_draftpicks(5, owner_team=old_team, team_season=4) + # await ctx.send(f'Okay, let me update these {len(all_picks)} owner teams..') + # + # for this_pick in [*all_picks.values()]: + # await patch_draftpick( + # this_pick['id'], + # owner_id=this_team['id'] + # ) + # + # await ctx.send(random_conf_gif()) + # + # @commands.command(name='fillgaps', help='Fill draft pick overall gaps', hidden=True) + # @commands.is_owner() + # async def fill_gaps_command(self, ctx): + # raw_picks = await get_draftpicks(season=5) + # all_picks = dict(sorted(raw_picks.items(), key=lambda item: item[1]["overall"])) + # overall = 1 + # + # for this_pick in [*all_picks.values()]: + # if this_pick['overall'] < 600: + # if this_pick['overall'] != overall: + # await patch_draftpick(this_pick['id'], overall=overall) + # logging.info(f'Updating {this_pick["origowner"]["abbrev"]} {this_pick["round"]}: ' + # f'#{this_pick["overall"]} to {overall}') + # overall += 1 + # + # @commands.command(name='dropmildem', help='Remove MiL demotion week', hidden=True) + # @commands.is_owner() + # async def drop_mil_demotion_command(self, ctx): + # current = await get_current() + # await ctx.send('I\'m fuckin on it') + # return_string = '' + # for x in range(99, 154): + # this_team = await get_one_team(x) + # if this_team['abbrev'][-3:] == 'MiL': + # return_string += f'Checking {this_team["abbrev"]}...\n' + # mil_players = await get_players(current['season'], this_team['abbrev']) + # count = 0 + # for y in mil_players: + # if mil_players[y]['demotion_week']: + # await patch_player(mil_players[y]['id'], demotion_week=False) + # count += 1 + # return_string += f'Eradicated {count} demotion weeks\n' + # + # await ctx.send(return_string) + # + # @commands.command(name='keepers', help='Set keepers', hidden=True) + # @commands.is_owner() + # async def keepers_command(self, ctx, team_abbrev, *, player_names): + # current = await get_current() + # player_list = [] + # + # this_team = await get_one_team(team_abbrev) + # player_names = re.split(',', player_names) + # keepers = [] + # keeper_count = 1 + # + # for x in player_names: + # player_cog = self.bot.get_cog('Players') + # player_name = await fuzzy_player_search(ctx, ctx.channel, self.bot, x, player_cog.player_list.keys()) + # player = await get_one_player(player_name) + # keepers.append(player["name"]) + # + # all_players = [] + # + # active_roster = await get_players(season=current['season'], team_abbrev=team_abbrev) + # il_players = await get_players(season=current['season'], team_abbrev=f'{team_abbrev}IL') + # mil_players = await get_players(season=current['season'], team_abbrev=f'{team_abbrev}MiL') + # + # for guy in active_roster.values(): + # all_players.append(guy) + # for guy in il_players.values(): + # all_players.append(guy) + # for guy in mil_players.values(): + # all_players.append(guy) + # + # all_picks = await get_draftpicks(season=current['season'], owner_team=this_team, round_end=7) + # # logging.info(f'\n{all_picks}\n') + # # sorted_picks = sorted(all_picks.items(), key=lambda item: item[1]["overall"]) + # sorted_picks = [] + # for p in all_picks: + # sorted_picks.append(all_picks[p]) + # # logging.info(f'{sorted_picks}') + # + # for x in all_players: + # logging.info(f'Checking {x["name"]} for keeper status') + # if x["name"] in keepers: + # logging.info(f'{x["name"]} is a keeper') + # draft_pick = sorted_picks[keeper_count - 1] + # logging.info(f'Setting {x["name"]} as {this_team["abbrev"]}\'s #{keeper_count} keeper with overall ' + # f'pick #{draft_pick["overall"]}') + # + # await patch_draftpick(draft_pick['id'], player_id=x['id']) + # await patch_player(x['id'], team_id=this_team['id'], demotion_week=2) + # await post_transactions([{ + # 'week': -1, + # 'player_id': x['id'], + # 'oldteam_id': 201, # FA team ID + # 'newteam_id': this_team['id'], + # 'season': current['season'], + # 'moveid': f'keeper-{this_team["abbrev"]}-{keeper_count}' + # }]) + # pick_num = draft_pick['overall'] % 16 + # if pick_num == 0: + # pick_num = 16 + # try: + # await self.send_pick_to_sheets(draft_pick, x, pick_num) + # except Exception as e: + # logging.error(f'{e}') + # keeper_count += 1 + # else: + # await patch_player(x['id'], team_id=201) + + +async def setup(bot): + await bot.add_cog(Draft(bot)) diff --git a/cogs/fun.py b/cogs/fun.py new file mode 100644 index 0000000..c04c771 --- /dev/null +++ b/cogs/fun.py @@ -0,0 +1,422 @@ +import math + +from helpers import * +import discord + +from peewee import * +from datetime import datetime, timedelta +from discord.ext import commands, tasks + +db = SqliteDatabase( + 'storage/sba_is_fun.db', + pragmas={ + 'journal_mode': 'wal', + 'cache_size': -1 * 64000, + 'synchronous': 0 + } +) + + +class Creator(Model): + name = CharField() + discordid = IntegerField() + + class Meta: + database = db + + +class Command(Model): + name = CharField() + message = CharField() + creator = ForeignKeyField(Creator) + createtime = DateTimeField() + last_used = DateTimeField() + sent_warns = IntegerField(default=0) + + class Meta: + database = db + + +class Roles(Model): + name = CharField(unique=True) + enabled = BooleanField(default=True) + + class Meta: + database = db + + +class Fun(commands.Cog): + def __init__(self, bot): + self.bot = bot + + db.create_tables([Creator, Command, Roles]) + db.close() + self.daily_check.start() + + @tasks.loop(hours=20) + async def daily_check(self): + try: + # logging.info(f'trying to start cc check') + guild = self.bot.get_guild(int(os.environ.get('GUILD_ID'))) + if not guild: + # logging.info(f'no guild found for cc check') + await asyncio.sleep(15) + guild = self.bot.get_guild(int(os.environ.get('GUILD_ID'))) + if not guild: + logging.error(f'Fun cog could not access guild') + return + except Exception as e: + logging.error(f'Could not run daily_check: {e}') + return + + # = {'member': , 'commands': [(, )]} + del_notifs = {} + del_counter = 0 + # = {'member': , 'commands': [(, )]} + warn_notifs = {} + now = datetime.now() + for x in Command.select(): + # Final check / deleted + if x.last_used + timedelta(days=90) < now: + logging.warning(f'Deleting `!cc {x.name}`') + owner = guild.get_member(x.creator.discordid) + if owner: + if owner.id not in del_notifs: + del_notifs[owner.id] = {'member': owner, 'commands': [(x.name, x.message)]} + else: + del_notifs[owner.id]['commands'].append((x.name, x.message)) + + # x.delete_instance() + del_counter += 1 + + elif x.last_used + timedelta(days=60) < now and (x.sent_warns is None or x.sent_warns == 0): + logging.warning(f'Warning for `!cc {x.name}`') + x.sent_warns = 1 + x.save() + owner = guild.get_member(x.creator.discordid) + if owner: + if owner.id not in warn_notifs: + warn_notifs[owner.id] = {'member': owner, 'commands': [(x.name, x.message)]} + else: + warn_notifs[owner.id]['commands'].append((x.name, x.message)) + + # else: + # logging.warning( + # f'Command last used {x.last_used} / delta: {now - x.last_used} \n/>60 days: ' + # f'{x.last_used + timedelta(days=60) < now} / sent_warns: {x.sent_warns}' + # ) + + db.close() + logging.info(f'deletions: {del_notifs}\nwarnings: {warn_notifs}') + + for member in del_notifs: + plural = len(del_notifs[member]["commands"]) > 1 + msg_content = f'Yo, it\'s cleanup time. I am deleting the following custom ' \ + f'command{"s" if plural else ""}:\n\n' + for x in del_notifs[member]["commands"]: + msg_content += f'`!cc {x[0]}` - {x[1]}\n' + await del_notifs[member]['member'].send(msg_content) + + for member in warn_notifs: + plural = len(warn_notifs[member]["commands"]) > 1 + msg_content = f'Heads up, the following custom ' \ + f'command{"s" if plural else ""} will be deleted next month if ' \ + f'{"they are" if plural else "it is"} not used:\n\n' + for x in warn_notifs[member]["commands"]: + msg_content += f'`!cc {x[0]}` - {x[1]}\n' + await warn_notifs[member]['member'].send(msg_content) + + logging.info(f'Deleted {del_counter} commands; sent deletion notifications to {len(del_notifs)} users; ' + f'sent warnings to {len(warn_notifs)} users') + + async def cog_command_error(self, ctx, error): + await ctx.send(f'{error}') + + @commands.command(name='cc', help='Run custom custom command') + async def custom_command(self, ctx, command): + chosen = Command.get_or_none(fn.Lower(Command.name) == command.lower()) + if not chosen: + # Error gif + # await ctx.send('https://tenor.com/blQnd.gif') + + # Schitt's Creek 'what's that' gif + # await ctx.send('https://media.giphy.com/media/l0HUhFZx6q0hsPtHq/giphy.gif') + + # Kermit lost gif + await ctx.send('https://tenor.com/6saQ.gif') + else: + await ctx.send(chosen.message) + chosen.last_used = datetime.now() + chosen.sent_warns = 0 + chosen.save() + + db.close() + + @commands.command(name='about', help='Who made the custom command') + async def about_command(self, ctx, command): + chosen = Command.get_or_none(fn.Lower(Command.name) == command.lower()) + if not chosen: + await ctx.send('https://tenor.com/blQnd.gif') + + embed = discord.Embed(title=f'About {chosen.name.title()}', color=0xFFFF00) + embed.add_field(name=f'Creator', value=f'{chosen.creator.name}', inline=False) + embed.add_field(name='Creation Date', value=f'{chosen.createtime}', inline=False) + embed.add_field(name='Message', value=f'{chosen.message}', inline=False) + + await ctx.send(content=None, embed=embed) + db.close() + + @commands.command(name='newcc', help='Create a new custom command') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, 'Paper Dynasty Players') + async def new_custom_command(self, ctx, name, *, message): + time = datetime.now() + command = name + comm_message = message + + chosen = Command.get_or_none(fn.Lower(Command.name) == command.lower()) + if chosen: + await ctx.send('There is already a command with that name!') + return + + embed = discord.Embed(title='Is this what you want?', color=0x91329F) + embed.add_field(name='Command Name', value=command, inline=False) + embed.add_field(name='Message', value=comm_message, inline=False) + + await ctx.send(content=None, embed=embed) + + view = Confirm(responders=[ctx.author]) + question = await ctx.send('Should I create this for you?', view=view) + await view.wait() + + if not view.value: + await question.edit(content='You keep thinking on it.', view=None) + return + + this_person = Creator.get_or_none(Creator.discordid == ctx.author.id) + if not this_person: + this_person = Creator(name=f'{ctx.author.name}', discordid=f'{ctx.author.id}') + this_person.save() + + this_command = Command(name=command, message=comm_message, createtime=time, creator=this_person, last_used=time) + if this_command.save() == 1: + await question.edit(content=f'`!cc {this_command.name}` is now a thing!', view=None) + else: + await question.edit(content='Hmm...I couldn\'t add that. I might need a grown up to help.', view=None) + + db.close() + + @commands.command(name='delcc', help='Delete a custom command') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, 'Paper Dynasty Players') + async def delete_custom_command(self, ctx, name): + this_command = Command.get_or_none(fn.Lower(Command.name) == name.lower()) + if not this_command: + await ctx.send('I couldn\'t find that command, sorry.') + return + + if this_command.creator.discordid != ctx.author.id and ctx.author.id != self.bot.owner_id: + await ctx.send('Looks like this isn\'t your command to delete.') + return + + embed = discord.Embed(title='Do you want to delete this command?', color=0x91329F) + embed.add_field(name='Command Name', value=this_command.name, inline=False) + embed.add_field(name='Message', value=this_command.message, inline=False) + + view = Confirm(responders=[ctx.author]) + question = await ctx.send(content=None, embed=embed, view=view) + await view.wait() + + if not view.value: + await question.edit(content='It stays for now.', view=None) + return + + if this_command.delete_instance() == 1: + await ctx.send('He gone!') + else: + await ctx.send('Welp. That didn\'t work. Go complain to an adult, I guess.') + + db.close() + + @commands.command(name='allcc', help='Show all custom commands') + async def show_custom_commands(self, ctx, page=1): + def get_embed(this_page): + this_embed = discord.Embed(title=f'All Custom Commands', color=0x2F939F) + column_one = '' + column_two = '' + all_commands = Command.select().paginate(this_page, 40).order_by(Command.name) + for x in range(20): + try: + column_one += f'**{all_commands[x].name}** by {all_commands[x].creator.name}\n' + except Exception as e: + logging.error(f'Error building !allcc embed: {e}') + break + this_embed.add_field(name=f'{(this_page - 1) * 40 + 1}-{this_page * 40 - 20}', value=column_one) + + for x in range(20, 40): + try: + column_two += f'**{all_commands[x].name}** by {all_commands[x].creator.name}\n' + except Exception as e: + logging.error(f'Error building !allcc embed: {e}') + break + if len(column_two) > 0: + this_embed.add_field(name=f'{(this_page - 1) * 40 + 21}-{this_page * 40}', value=column_two) + + return this_embed + + page_num = page + total_commands = Command.select(Command.id) + last_page = math.ceil(total_commands.count()/40) + + if page_num > last_page: + await ctx.send(f'The max page number is {last_page}; going there now!') + page_num = last_page + + embed = get_embed(page_num) + embed.description = f'Page {page_num} / {last_page}' + view = Pagination(responders=[ctx.author]) + resp_message = await ctx.send(content=None, embed=embed, view=view) + + while True: + await view.wait() + + if view.value: + logging.info(f'got a value: {view.value}') + if view.value == 'left': + page_num = page_num - 1 if page_num > 1 else last_page + if view.value == 'right': + page_num = page_num + 1 if page_num <= last_page else 1 + view.value = None + else: + await resp_message.edit(content=None, embed=embed, view=None) + break + + # await resp_message.edit(content=None, embed=embed, view=None) + embed = get_embed(page_num) + embed.description = f'Page {page_num} / {last_page}' + view = Pagination(responders=[ctx.author]) + await resp_message.edit(content=None, embed=embed, view=view) + + db.close() + + @commands.command(name='mycc', aliases=['showcc'], help='Show my commands') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, 'Paper Dynasty Players') + async def my_custom_commands(self, ctx): + this_creator = Creator.get_or_none(Creator.discordid == ctx.author.id) + + if not this_creator: + await ctx.send('It doesn\'t look like you\'ve created any custom commands. Try it out by running the ' + '!help newcc for the command syntax!') + return + + all_commands = Command.select().join(Creator).where(Command.creator == this_creator) + + if all_commands.count() == 0: + await ctx.send('It doesn\'t look like you\'ve created any custom commands. Try it out by running the ' + '!help newcc for the command syntax!') + return + + comm_message = '' + for x in all_commands: + comm_message += f'{x.name}\n' + + embed = discord.Embed(title=f'{ctx.author.name}\'s Commands', color=0x2F939F) + embed.add_field(name=f'Command Names', value=comm_message, inline=False) + + await ctx.send(content=None, embed=embed) + + db.close() + + # @commands.command(name='showcc', help='Show one person\'s custom commands') + # @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, 'Paper Dynasty Players') + # async def show_cc_command(self, ctx, ): + + # @commands.command(name='role', help='Toggle role') + # async def toggle_role_command(self, ctx, *, role_name): + # all_roles = [x.name for x in Roles.select().where(Roles.enabled)] + # + # async def toggle_role(full_role): + # if full_role in ctx.author.roles: + # await ctx.author.remove_roles(full_role) + # else: + # await ctx.author.add_roles(full_role) + # + # if len(role_name) < 4: + # await ctx.send('https://thumbs.gfycat.com/FrayedUnequaledGnat-size_restricted.gif') + # await ctx.send(f'What even is **{role_name}**...') + # db.close() + # return + # + # for name in all_roles: + # if role_name.lower() in name.lower(): + # try: + # this_role = discord.utils.get(ctx.guild.roles, name=name) + # await toggle_role(this_role) + # await ctx.send(random_conf_gif()) + # return + # except: + # await ctx.send(await get_emoji(ctx, 'fforrespect', False)) + # await ctx.send('I was not able to assign that role.') + # return + # + # await ctx.send(f'That doesn\'t sound familiar. **{role_name}**...did you make that shit up?') + + # @commands.command(name='showroles', help='Show toggleable roles') + # async def show_roles_command(self, ctx): + # all_roles = [x.name for x in Roles.select().where(Roles.enabled)] + # role_string = '\n- '.join(all_roles) + # + # embed = get_team_embed('Toggleable Roles', thumbnail=False) + # embed.description = 'Run !role to toggle the role on or off' + # embed.add_field(name='Role Names', value=f'- {role_string}') + # + # await ctx.send(content=None, embed=embed) + + # @commands.command(name='newrole', aliases=['removerole'], help='Make toggleable role') + # @commands.is_owner() + # async def make_toggleable_role_command(self, ctx, *, role_name): + # this_role = Roles.get_or_none(Roles.name == role_name) + # + # if not this_role: + # # Create the role if it doesn't exist + # + # this_role = Roles(name=role_name) + # this_role.save() + # if not discord.utils.get(ctx.guild.roles, name=this_role.name): + # await ctx.guild.create_role(name=f'{role_name}', mentionable=True) + # else: + # # Disable the role + # + # if this_role.enabled: + # this_role.enabled = False + # else: + # this_role.enabled = True + # this_role.save() + # this_role = discord.utils.get(ctx.guild.roles, name=this_role.name) + # + # if this_role: + # await this_role.edit(mentionable=False) + # else: + # await ctx.send('That role doesn\'t exist in the server.') + # + # await ctx.send(random_conf_gif()) + + # @commands.command(name='bulkrole', hidden=True) + # @commands.is_owner() + # async def bulkrole_command(self, ctx, *roles): + # all_roles = [] + # + # for x in roles: + # all_roles.append(discord.utils.get(ctx.guild.roles, name=x)) + # + # await ctx.send('On it. This could take a bit.') + # time_start = datetime.now() + # + # async for member in ctx.guild.fetch_members(): + # logging.warning(f'member: {member}') + # await member.add_roles(*all_roles) + # + # time_end = datetime.now() + # await ctx.send(f'All done! That took {time_end - time_start}') + + +async def setup(bot): + await bot.add_cog(Fun(bot)) diff --git a/cogs/gameday.py b/cogs/gameday.py new file mode 100644 index 0000000..13d9f16 --- /dev/null +++ b/cogs/gameday.py @@ -0,0 +1,2383 @@ +import re +from pandas import DataFrame + +from helpers import * +from db_calls import * +from db_calls_scouting import * +from db_calls_gameday import * + +import discord +from discord.ext import commands + + +class Gameday(commands.Cog): + def __init__(self, bot): + self.bot = bot + + async def cog_command_error(self, ctx, error): + await ctx.send(f'{error}') + + @staticmethod + def lineup_check(this_game, home=True): + if home: + team_id = this_game['home_team_id'] + team_name = 'home team' + else: + team_id = this_game['away_team_id'] + team_name = 'away team' + + full_lineup = get_lineup_members(this_game['id'], active=1, team_id=team_id) + roster_check = { + 'C': 0, + '1B': 0, + '2B': 0, + '3B': 0, + 'SS': 0, + 'LF': 0, + 'CF': 0, + 'RF': 0, + 'P': 0, + 'DH': 0, + 'batting_count': 0, + } + + for x in full_lineup['members']: + if x['position'] in roster_check.keys(): + roster_check[x["position"]] += 1 + if 0 < x['batting_order'] < 10: + roster_check['batting_count'] += 1 + + errors = [] + for key in roster_check: + if roster_check[key] > 1 and key != 'batting_count': + errors.append(f'{team_name} has {roster_check[key]}x {key}') + if roster_check[key] < 1 and key != 'DH': + errors.append(f'{team_name} has {roster_check[key]}x {key}') + if roster_check['batting_count'] != 9: + errors.append(f'{team_name} has {roster_check["batting_count"]} players in the batting order') + + logging.info(f'{"home" if home else "away"} error count: {len(errors)}') + return { + 'valid': True if len(errors) == 0 else False, + 'errors': errors + } + + async def game_state_ascii(self, this_game): + away_team = await get_one_team(this_game['away_team_id']) + home_team = await get_one_team(this_game['home_team_id']) + gs = get_game_state(this_game['id'])['states'][0] + occupied = '●' + unoccupied = '○' + + logging.info(f'checking baserunners') + first_base = unoccupied if not gs['on_first'] else occupied + second_base = unoccupied if not gs['on_second'] else occupied + third_base = unoccupied if not gs['on_third'] else occupied + logging.info(f'checking inning and score') + if gs['final']: + outs = '' + inning = 'FINAL' + else: + outs = f'{gs["outs"]} Out{"s" if gs["outs"] != 1 else ""}' + half = '▲' if gs['top_half'] else '▼' + inning = f'{half} {gs["inning"]}' + + logging.info(f'getting pitcher and batter members') + pbc_data = self.get_pbc(this_game) + batter_member = pbc_data['batter'] + pitcher_member = pbc_data['pitcher'] + logging.info(f'getting pitcher and batter players') + if not batter_member: + this_batter = {'name': 'BATTER NOT FOUND', 'image': LOGO} + else: + this_batter = await get_one_player(batter_member['player_id']) + if not pitcher_member: + this_pitcher = {'name': 'PITCHER NOT FOUND', 'image': LOGO} + else: + this_pitcher = await get_one_player(pitcher_member['player_id']) + # pitcher_name = this_pitcher['name'].split(' ', 1)[1] + # batter_name = this_batter['name'].split(' ', 1)[1] + + logging.info(f'setting player names') + # pitcher_name = this_pitcher['name'] if len(this_pitcher['name']) <= 19 else f'{this_pitcher["name"][:16]}...' + # batter_name = this_batter['name'] if len(this_batter['name']) <= 19 else f'{this_batter["name"][:16]}...' + logging.info(f'setting player stats') + era = f'{6.9:.2f}' + avg = f'{.420:.3f}' + if avg[0] == '0': + avg = avg[1:] + + logging.info(f'generating game string') + game_string = f'```\n' \ + f'{away_team["abbrev"]: ^4}{gs["away_score"]: ^3} {second_base}' \ + f'{inning: >12}\n' \ + f'{home_team["abbrev"]: ^4}{gs["home_score"]: ^3} {third_base} {first_base}' \ + f'{outs: >10}\n```' + return {'gs': game_string, 'batter': this_batter, 'pitcher': this_pitcher, 'away_team': away_team, + 'home_team': home_team, 'batter_member': batter_member, 'pitcher_member': pitcher_member} + + @staticmethod + def on_base_code(this_state): + if this_state['on_first'] and this_state['on_second'] and this_state['on_third']: + obc = 7 + elif this_state['on_second'] and this_state['on_third']: + obc = 6 + elif this_state['on_first'] and this_state['on_third']: + obc = 5 + elif this_state['on_first'] and this_state['on_second']: + obc = 4 + elif this_state['on_third']: + obc = 3 + elif this_state['on_second']: + obc = 2 + elif this_state['on_first']: + obc = 1 + else: + obc = 0 + + return obc + + @staticmethod + def increment_score(this_game, runs: int): + this_state = get_game_state(game_id=this_game['id'])['states'][0] + if this_state['top_half']: + new_state = patch_game_state( + this_state['id'], + away_score=this_state['away_score'] + runs + ) + else: + new_state = patch_game_state( + this_state['id'], + home_score=this_state['home_score'] + runs + ) + + return new_state + + @staticmethod + def increment_outs(this_game, outs: int): + this_state = get_game_state(game_id=this_game['id'])['states'][0] + + if this_state['outs'] + outs < 3: + new_outs = this_state['outs'] + outs + new_state = patch_game_state( + this_state['id'], + outs=new_outs + ) + elif this_state['top_half']: + new_state = patch_game_state( + this_state['id'], + outs=0, + top_half=False, + on_first_id=False, + on_second_id=False, + on_third_id=False + ) + else: + new_state = patch_game_state( + this_state['id'], + outs=0, + top_half=True, + inning=this_state['inning'] + 1, + on_first_id=False, + on_second_id=False, + on_third_id=False + ) + + return new_state + + def advance_runners(self, this_state, num_bases: int, error: int, force_only=False): + """ + Moves runners and gives them a run scored if necessary - does not update game state + + :param error: + :param this_state: + :param num_bases: + :param force_only: + :return: int: runs scored on play + """ + rbi = 0 + + # logging.info(f'this_state:\n{this_state}\n') + if this_state['on_third']: + if not force_only or (force_only and this_state['on_second'] and this_state['on_first']): + logging.info(f'runner {this_state["on_third"]["id"]} scores') + on_third = get_one_atbat( + game_id=this_state['game']['id'], batter_id=this_state['on_third']['player_id'] + ) + patch_atbat(on_third['id'], run=1, error=error) + patch_game_state(this_state['id'], on_third_id=False) + rbi += 1 + + # logging.info('pre-on_second') + if this_state['on_second']: + if not force_only or (force_only and this_state['on_first']): + if num_bases > 1: + logging.info(f'runner {this_state["on_second"]["id"]} scores') + on_second = get_one_atbat( + game_id=this_state['game']['id'], batter_id=this_state['on_second']['player_id'] + ) + patch_atbat(on_second['id'], run=1, error=error) + patch_game_state(this_state['id'], on_second_id=False) + rbi += 1 + else: + logging.info(f'runner {this_state["on_second"]["id"]} from second to third') + patch_game_state(this_state['id'], on_second_id=False, on_third_id=this_state['on_second']['id']) + + # logging.info('pre-on_first') + if this_state['on_first']: + if num_bases > 2: + logging.info(f'runner {this_state["on_first"]["id"]} scores') + on_first = get_one_atbat( + game_id=this_state['game']['id'], batter_id=this_state['on_first']['player_id'] + ) + patch_atbat(on_first['id'], run=1, error=error) + patch_game_state(this_state['id'], on_first_id=False) + rbi += 1 + elif num_bases == 2: + logging.info(f'runner {this_state["on_first"]["id"]} from first to third') + patch_game_state(this_state['id'], on_first_id=False, on_third_id=this_state['on_first']['id']) + else: + logging.info(f'runner {this_state["on_first"]["id"]} from first to second') + patch_game_state(this_state['id'], on_first_id=False, on_second_id=this_state['on_first']['id']) + + return rbi + + @staticmethod + def get_pbc(this_game): + """ + Return a dict including the pitcher, batter, and catcher + + :param this_game: + :return: + """ + this_state = get_game_state(game_id=this_game['id'])['states'][0] + + # Helpers for finding pitcher and batter + if this_state['top_half']: + bat_team_id = this_game['away_team_id'] + pit_team_id = this_game['home_team_id'] + batting_order = this_state['away_batter_up'] + else: + bat_team_id = this_game['home_team_id'] + pit_team_id = this_game['away_team_id'] + batting_order = this_state['home_batter_up'] + + # Get pitcher and batter + logging.info('get pitcher and batter') + try: + this_batter = get_one_member( + game_id=this_game['id'], team_id=bat_team_id, batting_order=batting_order, active=1 + ) + except: + this_batter = None + + try: + this_pitcher = get_one_member( + game_id=this_game['id'], team_id=pit_team_id, position='P', active=1 + ) + except: + this_pitcher = None + + try: + this_catcher = get_one_member( + game_id=this_game['id'], team_id=pit_team_id, position='C', active=1 + ) + except: + this_catcher = None + + return {'batter': this_batter, 'pitcher': this_pitcher, 'catcher': this_catcher} + + @staticmethod + def get_defender(this_game, pos): + this_state = get_game_state(game_id=this_game['id'])['states'][0] + + if this_state['top_half']: + this_member = get_one_member( + game_id=this_game['id'], + team_id=this_game['home_team_id'], + position=pos.upper(), + active=1 + ) + else: + this_member = get_one_member( + game_id=this_game['id'], + team_id=this_game['away_team_id'], + position=pos.upper(), + active=1 + ) + + return this_member + + @staticmethod + async def get_batter_statline(data): + batter_string = '' + bs = get_atbat_totals(data['batter_member']['id']) + if not bs['empty']: + batter_string += f' - {bs["hit"] if bs["hit"] else 0}-{bs["ab"] if bs["ab"] else 0}' + if bs["homerun"]: + batter_string += f', ' + if bs["homerun"] > 1: + batter_string += f'{bs["homerun"]} ' + batter_string += 'HR' + if bs["triple"]: + batter_string += f', ' + if bs["triple"] > 1: + batter_string += f'{bs["triple"]} ' + batter_string += '3B' + if bs["double"]: + batter_string += f', ' + if bs["double"] > 1: + batter_string += f'{bs["double"]} ' + batter_string += '2B' + if bs["bb"]: + batter_string += f', ' + if bs["bb"] > 1: + batter_string += f'{bs["bb"]} ' + batter_string += 'BB' + if bs["hbp"]: + batter_string += f', ' + if bs["hbp"] > 1: + batter_string += f'{bs["hbp"]} ' + batter_string += 'HBP' + if bs["sac"]: + batter_string += f', ' + if bs["sac"] > 1: + batter_string += f'{bs["sac"]} ' + batter_string += 'SAC' + if bs["so"]: + batter_string += f', ' + if bs["so"] > 1: + batter_string += f'{bs["so"]} ' + batter_string += 'K' + else: + b = await get_one_battingseason(data['batter_member']['player_id']) + batter_string = '1st PA' + if len(b) > 0: + if b['ab'] > 0: + singles = b['hit'] - b['hr'] - b['triple'] - b['double'] + avg = b['hit'] / b['ab'] + obp = (b['hit'] + b['bb'] + b['ibb'] + b['hbp']) / b['pa'] + slg = ((b['hr'] * 4) + (b['triple'] * 3) + (b['double'] * 2) + singles) / b['ab'] + ops = obp + slg + woba = ((b['bb'] * .69) + (b['hbp'] * .72) + (singles * .89) + (b['double'] * 1.27) + + (b['triple'] * 1.62) + (b['hr'] * 2.1)) / (b['pa'] - b['hbp'] - b['sac']) + ab = f'{b["ab"]:.0f}' + run = f'{b["run"]:.0f}' + hit = f'{b["hit"]:.0f}' + double = f'{b["double"]:.0f}' + top_stat = {'name': '2B', 'value': double, 'int': b['double']} + + triple = f'{b["triple"]:.0f}' + if b["triple"] > top_stat['int']: + top_stat['name'] = '3B' + top_stat['value'] = triple + top_stat['int'] = b['triple'] + + hr = f'{b["hr"]:.0f}' + if b["hr"] > top_stat['int']: + top_stat['name'] = 'HR' + top_stat['value'] = hr + top_stat['int'] = b['hr'] + + rbi = f'{b["rbi"]:.0f}' + sb = f'{b["sb"]:.0f}' + if b["sb"] > top_stat['int']: + top_stat['name'] = 'SB' + top_stat['value'] = sb + top_stat['int'] = b['sb'] + + so = f'{b["so"]:.0f}' + + batter_string = f' - {avg:.3f} / {obp:.3f} / {slg:.3f}, {top_stat["value"]} {top_stat["name"]}' + + # batting_string = f'```\n' \ + # f' AVG OBP SLG OPS\n' \ + # f' {avg:.3f} {obp:.3f} {slg:.3f} {ops:.3f}\n``````\n' \ + # f' AB R H 2B 3B HR RBI SB SO\n' \ + # f'{ab: >3} {run: >2} {hit: ^3} {double: >2} {triple: >2} {hr: >2} {rbi: >3} ' \ + # f'{sb: >2} {so: ^3}\n```' + + logging.info(f'batter_string: {batter_string}') + return batter_string + + @staticmethod + async def get_pitcher_statline(data): + pitcher_string = '' + bs = get_atbat_pitching_totals(data['pitcher_member']['id']) + if not bs['empty']: + pitcher_string += f'{bs["ip"]:.1f} IP' + if bs["run"]: + pitcher_string += f', {bs["run"]} R' + if bs["run"] != bs["erun"]: + pitcher_string += f' ({bs["erun"]} ER)' + if bs["so"]: + pitcher_string += f', {bs["so"]} K' + if bs["hit"]: + pitcher_string += f', {bs["hit"]} H' + if bs["bb"]: + pitcher_string += f', {bs["bb"]} BB' + if bs["hbp"]: + pitcher_string += f', {bs["hbp"]} HBP' + if bs["hr"]: + pitcher_string += f', {bs["hbp"]} HR' + else: + p = await get_one_pitchingseason(data['pitcher_member']['player_id']) + if len(p) > 0: + if p['ip'] > 0: + win = f'{p["win"]:.0f}' + loss = f'{p["loss"]:.0f}' + save = f'{p["sv"]:.0f}' + era = f'{(p["erun"] * 9) / p["ip"]:.2f}' + game = f'{p["game"]:.0f}' + gs = f'{p["gs"]:.0f}' + ip = f'{p["ip"]:.0f}' + if p["ip"] % 1 == 0: + ip += '.0' + elif str(p["ip"] % 1)[2] == '3': + ip += '.1' + else: + ip += '.2' + so = f'{p["so"]:.0f}' + whip = f'{(p["bb"] + p["hit"]) / p["ip"]:.2f}' + + g_string = f'{gs} GS' if p['game'] == p['gs'] else f'{game} G' + pitcher_string = f' - {g_string}, ({win}-{loss})' + if p["sv"]: + pitcher_string += f', {save} SV' + pitcher_string += f', {era} ERA' + + # pitching_string = f'```\n' \ + # f' W L SV ERA IP SO WHIP\n' \ + # f'{win: >2} {loss: >2} {save: >2} {era: >5} {ip: >5} ' \ + # f'{so: >3} {whip: >4}\n```' + + logging.info(f'pitcher_string: {pitcher_string}') + return pitcher_string + + @staticmethod + def adv_specific_runner(this_game, from_base, num_bases): + """ + Moves a single runner and gives them a run scored if necessary - does not update game state + + :param this_game: + :param from_base: + :param num_bases: + :return: + """ + this_state = get_game_state(game_id=this_game['id'])['states'][0] + rbi = 0 + if from_base == 3 and this_state['on_third']: + logging.info(f'runner {this_state["on_third"]["id"]} scores') + this_runner = get_atbat( + game_id=this_state['game']['id'], batter_id=this_state['on_third']['player_id'] + )['atbats'] + on_third = this_runner[len(this_runner) - 1] + patch_atbat(on_third['id'], run=1) + patch_game_state(this_state['id'], on_third_id=False) + rbi += 1 + + elif from_base == 2 and this_state['on_second']: + if num_bases > 1: + logging.info(f'runner {this_state["on_second"]["id"]} scores') + this_runner = get_atbat( + game_id=this_state['game']['id'], batter_id=this_state['on_second']['player_id'] + )['atbats'] + on_second = this_runner[len(this_runner) - 1] + patch_atbat(on_second['id'], run=1) + patch_game_state(this_state['id'], on_second_id=False) + rbi += 1 + else: + logging.info(f'runner {this_state["on_second"]["id"]} from second to third') + patch_game_state(this_state['id'], on_second_id=False, on_third_id=this_state['on_second']['id']) + + elif from_base == 1 and this_state['on_first']: + if num_bases > 2: + logging.info(f'runner {this_state["on_first"]["id"]} scores') + this_runner = get_atbat( + game_id=this_state['game']['id'], batter_id=this_state['on_first']['player_id'] + )['atbats'] + on_first = this_runner[len(this_runner) - 1] + patch_atbat(on_first['id'], run=1) + patch_game_state(this_state['id'], on_first_id=False) + rbi += 1 + elif num_bases == 2: + logging.info(f'runner {this_state["on_first"]["id"]} from first to third') + patch_game_state(this_state['id'], on_first_id=False, on_third_id=this_state['on_first']['id']) + else: + logging.info(f'runner {this_state["on_first"]["id"]} from first to second') + patch_game_state(this_state['id'], on_first_id=False, on_second_id=this_state['on_first']['id']) + + return rbi + + async def game_state_embed(self, this_game, full_length=True): + data = await self.game_state_ascii(this_game) + + embed = discord.Embed(title=f'{data["away_team"]["sname"]} @ {data["home_team"]["sname"]}') + embed.add_field(name='Game State', value=data['gs'], inline=False) + + pitcher_string = f'[{data["pitcher"]["name"]}]({get_player_url(data["pitcher"])}) ' \ + f'{await self.get_pitcher_statline(data)}' + # embed.add_field(name='Pitcher', value=pitcher_string) + + batter_string = f'[{data["batter"]["name"]}]({get_player_url(data["batter"])}) ' \ + f'{await self.get_batter_statline(data)}' + # embed.add_field(name='Batter', value=batter_string) + embed.add_field(name='Matchup', value=f'Pitcher: {pitcher_string}\nvs\nBatter: {batter_string}', inline=False) + + embed.set_thumbnail(url=f'{data["pitcher"]["image"]}') + embed.set_image(url=f'{data["batter"]["image"]}') + + this_state = get_game_state(game_id=this_game['id'])['states'][0] + baserunner_string = '' + if this_state['on_first']: + runner = await get_one_player(this_state['on_first']['player_id']) + # embed.add_field(name='On First', value=f'[{runner["name"]}]({get_player_url(runner)})') + baserunner_string += f'On First: [{runner["name"]}]({get_player_url(runner)})\n' + if this_state['on_second']: + runner = await get_one_player(this_state['on_second']['player_id']) + # embed.add_field(name='On Second', value=f'[{runner["name"]}]({get_player_url(runner)})') + baserunner_string += f'On Second: [{runner["name"]}]({get_player_url(runner)})\n' + if this_state['on_third']: + runner = await get_one_player(this_state['on_third']['player_id']) + # embed.add_field(name='On Third', value=f'[{runner["name"]}]({get_player_url(runner)})') + baserunner_string += f'On Third: [{runner["name"]}]({get_player_url(runner)})\n' + if len(baserunner_string) > 0: + embed.add_field(name='Baserunners', value=baserunner_string, inline=False) + + if not full_length: + return embed + + async def lineup_string(team): + full_lineup = get_lineup_members(this_game['id'], active=1, team_id=team['id']) + sorted_lineup = sorted(full_lineup['members'], key=lambda item: item['batting_order']) + l_string = '' + for x in sorted_lineup: + player = await get_one_player(x["player_id"]) + l_string += f'{x["batting_order"]}. [{player["name"]}]({get_player_url(player)}) - ' \ + f'{x["position"]}\n' + if len(l_string) == 0: + l_string = 'None, yet\n\'!sl\' to set' + return l_string + + embed.add_field( + name=f'{data["away_team"]["abbrev"]} Lineup', + value=await lineup_string(data["away_team"]) + ) + embed.add_field( + name=f'{data["home_team"]["abbrev"]} Lineup', + value=await lineup_string(data["home_team"]) + ) + + return embed + + def log_play_core(self, this_game, bases=0, outs=0, ab=1, hit=0, error=0, force_only=False): + """ + Logs an AtBat, taking the optional parameters into account; does not place batter runner + + :param error: + :param this_game: + :param bases: + :param outs: + :param ab: + :param hit: + :param force_only: + :return: {'batter': LineupMember, 'ab': AtBat} + """ + this_state = get_game_state(game_id=this_game['id'])['states'][0] + + data = self.get_pbc(this_game) + this_batter = data['batter'] + this_pitcher = data['pitcher'] + + # Get OBC and patch GameState score if necessary + logging.info('Get OBC and patch GameState score if necessary') + obc = self.on_base_code(this_state) + + rbi = 0 + next_batters = { + 'away': this_state['away_batter_up'] + 1 if this_state['away_batter_up'] < 9 else 1, + 'home': this_state['home_batter_up'] + 1 if this_state['home_batter_up'] < 9 else 1, + } + new_half = this_state['top_half'] + new_inning = this_state['inning'] + if this_state['outs'] + outs < 3: + new_outs = this_state['outs'] + outs + + # Advance runners + logging.info('Advance runners') + if obc > 0 and bases > 0: + rbi = self.advance_runners(this_state, bases, error=error, force_only=force_only) + if bases == 4: + rbi += 1 + else: + # Ends half inning + new_outs = 0 + new_half = not this_state['top_half'] + if new_half: + new_inning += 1 + patch_game_state( + this_state['id'], + on_first_id=False, + on_second_id=False, + on_third_id=False + ) + + if this_state['top_half']: + # Update score, batter up, and runner on first + logging.info(f'current batter: {this_state["away_batter_up"]} / next: {next_batters["away"]}') + patch_game_state( + this_state['id'], + away_score=this_state['away_score'] + rbi, + away_batter_up=next_batters['away'], + outs=new_outs, + top_half=new_half, + inning=new_inning + ) + else: + patch_game_state( + this_state['id'], + home_score=this_state['home_score'] + rbi, + home_batter_up=next_batters['home'], + outs=new_outs, + top_half=new_half, + inning=new_inning + ) + + logging.info(f'post AB:\ngame_id: {this_game["id"]}\nbatter_id: {this_batter["player_id"]}\n' + f'pitcher_id: {this_pitcher["player_id"]}\nobc: {obc}\ninning: {this_state["inning"]}\n' + f'hit: {hit}\nrbi: {rbi}\nab: {ab}\nerror: {error}') + this_ab = post_atbat({ + 'game_id': this_game['id'], + 'batter_id': this_batter['player_id'], + 'pitcher_id': this_pitcher['player_id'], + 'on_base_code': obc, + 'inning': this_state['inning'], + 'hit': hit, + 'rbi': rbi if not error else 0, + 'ab': ab, + 'error': error + }) + + return {'batter': this_batter, 'ab': this_ab} + + async def get_csv_batters(self, this_game, team_id): + bats = [] + + for x in get_lineup_members(this_game['id'], team_id=team_id)['members']: + this_player = await get_one_player(x['player_id']) + bs = get_atbat_totals(x['id']) + rs = get_running_totals(x['id']) + ch = get_chaos_totals(x['id']) + ds = get_defense_totals(x['id']) + + if bs['empty'] and rs['empty'] and ch['empty'] and len(ds['defense']) == 0: + logging.info(f'No stats for {this_player["name"]} - moving on') + else: + dpos = None + if len(ds['defense']) == 1: + logging.info('one defense found') + if ds['defense'][0]['pos'] == x['position']: + logging.info('matches position') + dpos = ds['defense'][0] + else: + logging.info('doesn\'t match position') + line = ds['defense'][0] + bats.append([ + this_player['name'], this_player['team']['abbrev'], x['position'], '', '', '', + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', line['xch'], + line['xch'], + line['xch'], line['xhit'], line['errors'], '', line['sba'], line['csc'], line['roba'], + line['robs'], line['raa'], line['rto'] + ]) + else: + logging.info('many defenses found') + for line in ds['defense']: + if line['pos'] == line['pos']: + logging.info('this one matches pos') + dpos = line + else: + logging.info('this one does not matche pos') + bats.append([ + this_player['name'], this_player['team']['abbrev'], x['position'], '', '', '', + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', line['xch'], + line['xch'], + line['xch'], line['xhit'], line['errors'], '', line['sba'], line['csc'], line['roba'], + line['robs'], line['raa'], line['rto'] + ]) + if not dpos: + logging.info('no defense found; adding blanks') + dpos = { + 'xch': '', + 'xhit': '', + 'errors': '', + 'sba': '', + 'csc': '', + 'roba': '', + 'robs': '', + 'raa': '', + 'rto': '', + } + logging.info('done with defense') + + bats.append([ + this_player['name'], this_player['team']['abbrev'], x['position'], bs['pa'], bs['ab'], bs['run'], + bs['hit'], bs['rbi'], bs['double'], bs['triple'], bs['homerun'], bs['bb'], bs['so'], + bs['hbp'], bs['sac'], bs['ibb'], bs['gidp'], rs['sb'], rs['cs'], bs['bphr'], bs['bpfo'], bs['bp1b'], + bs['bplo'], '', '', dpos['xch'], dpos['xhit'], dpos['errors'], + ch['pb'] if x['position'] == 'C' else '', dpos['sba'], dpos['csc'], '', '', dpos['raa'], dpos['rto'] + ]) + return bats + + async def get_csv_pitchers(self, this_game, team_id): + arms = [] + + for x in get_lineup_members(this_game['id'], team_id=team_id, position='P')['members']: + this_player = await get_one_player(x['player_id']) + bs = get_atbat_pitching_totals(x['id']) + cs = get_chaos_totals(x['id']) + + if bs['empty'] and cs['empty']: + logging.info(f'No stats for {this_player["name"]} - moving on') + else: + arms.append([ + this_player['name'], this_player['team']['abbrev'], bs['ip'], bs['hit'], bs['run'], bs['erun'], + bs['so'], bs['bb'], bs['hbp'], cs['wp'], cs['bk'], bs['hr'] + ]) + return arms + + async def get_game_results(self, this_game, combined=False): + away_bats = await self.get_csv_batters(this_game, this_game['away_team_id']) + away_arms = await self.get_csv_pitchers(this_game, this_game['away_team_id']) + home_bats = await self.get_csv_batters(this_game, this_game['home_team_id']) + home_arms = await self.get_csv_pitchers(this_game, this_game['home_team_id']) + + if combined: + batters = [['player', 'team', 'pos', 'pa', 'ab', 'run', 'hit', 'rbi', '2b', '3b', 'hr', 'bb', 'so', 'hbp', + 'sac', 'ibb', 'gidp', 'sb', 'cs', 'bphr', 'bpfo', 'bp1b', 'bplo', 'xba', 'xbt', 'xch', 'xhit', 'e', + 'pb', 'sba', 'csc', 'roba', 'robs', 'raa', 'rto']] + for x in away_bats: + batters.append(x) + for x in home_bats: + batters.append(x) + + pitchers = [['pitcher', 'team', 'ip', 'h', 'r', 'er', 'so', 'bb', 'hbp', 'wp', 'bk', 'hr', 'ir', 'irs', + 'gs', 'win', 'loss', 'hold', 'save', 'bs']] + for x in away_arms: + pitchers.append(x) + for x in home_arms: + pitchers.append(x) + + return {'batters': batters, 'pitchers': pitchers} + else: + return {'ab': away_bats, 'aa': away_arms, 'hb': home_bats, 'ha': home_arms} + + async def get_game_sheet(self, home_team, away_team, results, player_name): + sheets = pygsheets.authorize(service_file='storage/major-domo-service-creds.json') + results_sheet = sheets.drive.copy_file( + '1e_o5Fg0-Q9sp09btesBpXA_4lUtbNkRngQ32_Ie9_iI', + f'{away_team["abbrev"]} @ {home_team["abbrev"]} for {player_name}', + '1G0XJvlUb2cIGjD3b2-XT89TSwAOZmzaz') + sheet_id = results_sheet['id'] + + results_tab = sheets.open_by_key(sheet_id).worksheet_by_title('Results') + results_tab.update_values( + crange='A3', values=results['ab'] + ) + results_tab.update_values( + crange='A33', values=results['hb'] + ) + results_tab.update_values( + crange='A22', values=results['aa'] + ) + results_tab.update_values( + crange='A52', values=results['ha'] + ) + + return sheet_id + + @commands.command(name='newgame', aliases=['ng'], help='Start a new game') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def new_game_command(self, ctx): + current = await get_current() + this_team = await get_team_by_owner(current['season'], ctx.author.id) + + await ctx.send('**Note:** Make sure this is the channel where you will be playing commands. Once this is set, ' + 'I will only take gameplay commands here.') + + async def get_team(which='home'): + prompt = f'Please enter the abbrev of the {which} team.' + this_q = Question(self.bot, ctx.channel, prompt, qtype='text', timeout=15) + resp = await this_q.ask([ctx.author]) + + if not resp: + await ctx.send('You keep thinking about it and hit me up later if you figure it out.') + return None + else: + other_team = await get_one_team(resp) + if not other_team: + await ctx.send(f'What\'s a **{resp}**? If you could go ahead and run this command again, that\'d ' + f'be great.') + return None + else: + return other_team + + if not this_team: + await ctx.send('Hmm...I can\'t find your team. Are you from around here?') + return + + try: + this_matchup = await get_one_schedule( + season=current['season'], + team_abbrev1=this_team['abbrev'], + week=current['week'] + ) + except ValueError as e: + home_team = await get_team('home') + away_team = await get_team('away') + if not home_team or not away_team: + return + else: + away_team = this_matchup['awayteam'] + home_team = this_matchup['hometeam'] + + prompt = f'Would you like to start {away_team["abbrev"]} @ {home_team["abbrev"]} now?' + this_q = Question(self.bot, ctx.channel, prompt, 'yesno', 15) + resp = await this_q.ask([ctx.author]) + + if not resp: + home_team = await get_team('home') + away_team = await get_team('away') + if not home_team or not away_team: + return + + this_game = post_game({ + 'away_team_id': away_team['id'], + 'home_team_id': home_team['id'], + 'channel_id': ctx.channel.id + }) + await ctx.send(random_conf_gif()) + await ctx.send('Go ahead and set lineups with the !setlineup command') + + @commands.command(name='endgame', aliases=['eg'], help='End game in channel') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def end_game_command(self, ctx): + this_game = get_game(channel_id=ctx.channel.id, active=1) + gs_embed = await self.game_state_embed(this_game) + + prompt = 'Is this the game you would like to end?' + this_q = Question(self.bot, ctx.channel, prompt, 'yesno', 15, gs_embed) + resp = await this_q.ask([ctx.author]) + end_proper = True + + if not resp: + await ctx.send('Okay, we can leave it active for now.') + return + else: + away_team = await get_one_team(this_game['away_team_id']) + home_team = await get_one_team(this_game['home_team_id']) + + try: + async with ctx.typing(): + results = await self.get_game_results(this_game) + sheet_id = await self.get_game_sheet(home_team, away_team, results, ctx.author.display_name) + except pygsheets.InvalidArgumentValue as e: + prompt = 'There was an error creating the scorecard. Should I just nuke this game?' + this_q.prompt = prompt + resp = await this_q.ask([ctx.author]) + + if not resp: + await ctx.send('Okay, we can leave it active for now.') + return + else: + end_proper = False + + if end_proper: + await ctx.send(f'Here is the results sheet for this game: https://docs.google.com/spreadsheets/d/' + f'{sheet_id}') + else: + await ctx.send(random_conf_gif()) + patch_game(game_id=this_game['id'], active=False) + + @commands.command(name='setlineup', aliases=['sub', 'sl'], help='!sl ') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def set_lineup_command(self, ctx, team_abbrev, *, lineup): + ERROR_MESSAGE = 'The format for a lineup is: --\n' \ + 'Examples:\n2-Trea Turner-SS\n10-Rich Hill-P\n\n' \ + 'You may set multiple spots by adding a comma at the end of the position and, without ' \ + 'spaces, entering the next spot.\n' \ + 'Examples:\n1-Ronald Acuna Jr-RF,2-Trea Turner-SS,3-Robinson Cano-2B\n' \ + '7-Travis Jankowski-LF,10-Anthony Bass-P,5-Erik Gonzalez-3B' + + # current = await get_current() + this_team = await get_one_team(team_abbrev) + this_game = get_game(channel_id=ctx.channel.id, active=1) + logging.info(f'this_game: {this_game}') + if this_team['id'] not in [this_game['away_team_id'], this_game['home_team_id']]: + away_team = await get_one_team(this_game['away_team_id']) + home_team = await get_one_team(this_game['home_team_id']) + await ctx.send(f'This channel is for {away_team["abbrev"]} @ {home_team["abbrev"]} - I can\'t set a lineup ' + f'for **{team_abbrev.upper()}**.') + return + + async with ctx.typing(): + for member in re.split(',', lineup): + data = re.split('-', member) + this_player = await get_one_player(data[1]) + + # Check if player is already in game + try: + this_member = get_one_member(game_id=this_game['id'], player_id=this_player['id'], active=1) + except: + this_member = None + + # If already in game, just change position + if this_member: + # Stop sub if changing batting order - unless pitcher is entering the lineup + if this_member['batting_order'] != int(data[0]) and this_member['batting_order'] != 10: + await ctx.send(f'{this_player["name"]} is already in the game batting ' + f'#{this_member["batting_order"]} so they cannot move in the order.') + return + + new_member = patch_lineup_member( + member_id=this_member['id'], + position=data[2].upper(), + batting_order=data[0] + ) + + # Else add new lineup member + else: + new_member = post_lineup_member({ + 'game_id': this_game['id'], + 'team_id': this_team['id'], + 'player_id': this_player['id'], + 'position': data[2].upper(), + 'batting_order': data[0] + }) + + members = get_lineup_members(this_game['id'], this_team['id'], batting_order=data[0], active=1) + if len(members['members']) > 1: + for x in range(len(members['members'])): + if members['members'][x]['id'] != new_member['id']: + old_member = patch_lineup_member(members['members'][x]['id'], active=False) + + this_state = get_one_game_state(game_id=this_game['id']) + logging.info(f'old_member_id: {old_member["id"]} / on_first: {this_state["on_first"]} / ' + f'on_second: {this_state["on_second"]} / on_third: {this_state["on_third"]}') + if old_member in [this_state['on_first'], this_state['on_second'], this_state['on_third']]: + if old_member == this_state['on_first']: + this_state = patch_game_state( + this_state['id'], + on_first_id=new_member['id'] + ) + elif old_member == this_state['on_second']: + this_state = patch_game_state( + this_state['id'], + on_second_id=new_member['id'] + ) + elif old_member == this_state['on_third']: + this_state = patch_game_state( + this_state['id'], + on_third_id=new_member['id'] + ) + + sub_ab = post_atbat({ + 'game_id': this_game['id'], + 'batter_id': new_member['player_id'], + 'pitcher_id': self.get_pbc(this_game)['pitcher']['player_id'], + 'on_base_code': 0, + 'inning': this_state['inning'], + 'pa': 0 + }) + + # If player appears elsewhere in lineup, deactive old members + dupes = get_lineup_members( + this_game['id'], team_id=this_team['id'], player_id=this_player['id'], active=1 + ) + if len(dupes['members']) > 1: + for x in range(len(dupes['members']) - 1): + patch_lineup_member(dupes['members'][x]['id'], active=False) + + home_check = self.lineup_check(this_game, True) + away_check = self.lineup_check(this_game, False) + + check_string = f'Away lineup valid: {away_check["valid"]}\n' + if len(away_check['errors']) > 0: + error_string = "\n- ".join(away_check["errors"]) + check_string += f'- {error_string}\n\n' + + check_string += f'Home lineup valid: {home_check["valid"]}\n' + if len(home_check['errors']) > 0: + error_string = "\n- ".join(home_check["errors"]) + check_string += f'- {error_string}\n\n' + + if away_check['valid'] and home_check['valid']: + logging.info('Both lineups valid - sending gamestate') + content = None + embed = await self.game_state_embed(this_game, full_length=False) + else: + logging.info('Both lineups are not valid - sending check_string') + content = check_string + embed = None + + await ctx.send(content=content, embed=embed) + + @commands.command(name='gamestate', aliases=['gs'], help='Show game state') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def game_state_command(self, ctx): + # current = await get_current() + this_game = get_game(channel_id=ctx.channel.id, active=1) + + await ctx.send(content=None, embed=await self.game_state_embed(this_game)) + + @commands.group(name='log', help='`!help log` for all commands') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def log_play(self, ctx): + if ctx.invoked_subcommand is None: + await ctx.send('No play details listed. Run `!help log` for available commands.') + + @log_play.command( + name='single', + aliases=['si', 'si*', 'si**', 'siwh', '1b', '1b*', 'bp1b', 'bpsi', '1b**', '1bwh', 'single*', 'single**', + 'singlewh'], + help='si, 1b, bp1b, bpsi, single, *, **, wh' + ) + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def log_single_command(self, ctx): + this_game = get_game(channel_id=ctx.channel.id, active=1) + + cmd = ctx.message.content.lower() + + # Get baserunner advancement + if 'wh' in cmd or '**' in cmd: + bases = 2 + else: + bases = 1 + + data = self.log_play_core(this_game, bases=bases, hit=1) + patch_game_state( + this_game['id'], + on_first_id=data['batter']['id'] + ) + + if 'bp' in cmd: + patch_atbat( + data['ab']['id'], + bp1b=1 + ) + + await ctx.send(content=None, embed=await self.game_state_embed(this_game, full_length=False)) + + @log_play.command( + name='double', + aliases=['do', 'do**', 'do***', 'dowh', '2b', '2b**', '2b***', '2bwh', 'double**', 'double***', 'doublewh'], + help='do, 2b, double, **, ***, wh' + ) + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def log_double_command(self, ctx): + this_game = get_game(channel_id=ctx.channel.id, active=1) + + # Get baserunner advancement + if 'wh' in ctx.message.content.lower() or '***' in ctx.message.content: + bases = 3 + else: + bases = 2 + + data = self.log_play_core(this_game, bases=bases, hit=1) + patch_atbat( + data['ab']['id'], + double=1, + ) + patch_game_state( + this_game['id'], + on_second_id=data['batter']['id'] + ) + + await ctx.send(content=None, embed=await self.game_state_embed(this_game, full_length=False)) + + @log_play.command(name='triple', aliases=['tr', '3b'], help='tr, 3b') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def log_triple_command(self, ctx): + this_game = get_game(channel_id=ctx.channel.id, active=1) + + # Get baserunner advancement + bases = 3 + + data = self.log_play_core(this_game, bases=bases, hit=1) + patch_atbat( + data['ab']['id'], + triple=1, + ) + patch_game_state( + this_game['id'], + on_third_id=data['batter']['id'] + ) + + await ctx.send(content=None, embed=await self.game_state_embed(this_game, full_length=False)) + + @log_play.command(name='homerun', aliases=['hr', 'bphr'], help='hr, bphr') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def log_homerun_command(self, ctx): + this_game = get_game(channel_id=ctx.channel.id, active=1) + + # Get baserunner advancement + ballpark = 1 if 'bphr' in ctx.message.content.lower() else 0 + data = self.log_play_core(this_game, bases=4, hit=1) + patch_atbat( + data['ab']['id'], + run=1, + rbi=data['ab']['rbi'], + homerun=1, + bphr=ballpark + ) + + await ctx.send(content=None, embed=await self.game_state_embed(this_game, full_length=False)) + + @log_play.command(name='sacrifice', aliases=['sacf', 'sacb'], help='sacf, sacb') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def log_sac_command(self, ctx): + this_game = get_game(channel_id=ctx.channel.id, active=1) + gs = get_game_state(game_id=this_game['id'])['states'][0] + + rbi = 0 + sac = 0 + bases = 0 + ab = 0 + if gs['outs'] < 2: + cmd = ctx.message.content.lower() + sac = 1 + ab = 0 + if 'sacf' in cmd: + rbi = self.adv_specific_runner(this_game, 3, 1) + elif 'sacb' in cmd: + bases = 1 + + data = self.log_play_core(this_game, bases=bases, outs=1, ab=ab) + patch_atbat( + atbat_id=data['ab']['id'], + rbi=rbi, + sac=sac + ) + if rbi: + self.increment_score(this_game, rbi) + + await ctx.send(content=None, embed=await self.game_state_embed(this_game, full_length=False)) + + @log_play.command(name='walk', aliases=['bb', 'hbp', 'ibb'], help='bb, hbp, ibb') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def log_walk_command(self, ctx): + this_game = get_game(channel_id=ctx.channel.id, active=1) + + data = self.log_play_core(this_game, bases=1, ab=0, force_only=True) + patch_game_state( + this_game['id'], + on_first_id=data['batter']['id'] + ) + + cmd = ctx.message.content.lower() + if 'ibb' in cmd: + patch_atbat( + data['ab']['id'], + ibb=1, + ) + elif 'walk' in cmd or 'bb' in cmd: + patch_atbat( + data['ab']['id'], + bb=1, + ) + elif 'hbp' in cmd: + patch_atbat( + data['ab']['id'], + hbp=1, + ) + + await ctx.send(content=None, embed=await self.game_state_embed(this_game, full_length=False)) + + @log_play.command(name='sb_attempt', aliases=['sb', 'cs'], help='sb, cs') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def log_sb_command(self, ctx): + this_game = get_game(channel_id=ctx.channel.id, active=1) + gs = get_game_state(game_id=this_game['id'])['states'][0] + + if not gs['on_first'] and not gs['on_second'] and not gs['on_third']: + await ctx.send('I don\'t see any baserunners aboard - maybe this isn\'t a stolen base.') + return + + runner_found = None + this_runner = None + count = 4 + for runner in [gs['on_third'], gs['on_second'], gs['on_first']]: + count -= 1 + if runner: + a_runner = await get_one_player(runner['player_id']) + prompt = f'Did {a_runner["name"]} attempt the steal?' + this_q = Question(self.bot, ctx.channel, prompt, 'yesno', 10) + resp = await this_q.ask([ctx.author]) + if resp: + runner_found = count + this_runner = runner + break + + if not runner_found: + await ctx.send('If nobody was forced out - it must not have been a fielder\'s choice.') + return + + cmd = ctx.message.content.upper() + stolen_base = 1 if 'SB' in cmd else 0 + battery = self.get_pbc(this_game) + + post_running({ + 'game_id': this_game['id'], + 'runner_id': this_runner['player_id'], + 'stolen_base': stolen_base, + 'caught_stealing': not stolen_base + }) + + post_defense({ + 'game_id': this_game['id'], + 'player_id': battery['catcher']['player_id'], + 'position': 'C', + 'stolen_base_attempt': 1, + 'caught_stealing': not stolen_base, + }) + + if stolen_base: + if runner_found == 1: + patch_game_state( + state_id=gs['id'], + on_second_id=runner['id'], + on_first_id=False + ) + elif runner_found == 2: + patch_game_state( + state_id=gs['id'], + on_third_id=runner['id'], + on_second_id=False + ) + else: + patch_game_state( + state_id=gs['id'], + on_third_id=False + ) + self.increment_score(this_game, 1) + else: + if runner_found == 1: + patch_game_state( + state_id=gs['id'], + on_first_id=False + ) + elif runner_found == 2: + patch_game_state( + state_id=gs['id'], + on_second_id=False + ) + else: + patch_game_state( + state_id=gs['id'], + on_third_id=False + ) + self.increment_outs(this_game, 1) + + await ctx.send(content=None, embed=await self.game_state_embed(this_game, full_length=False)) + + # @log_play.command(name='extra_base', aliases=['xba', 'xbt'], help='xba, xbt') + # @commands.has_any_role(SBA_PLAYERS_ROLE_NAME) + # async def log_xba_command(self, ctx): + # this_game = get_game(channel_id=ctx.channel.id, active=1) + # gs = get_game_state(game_id=this_game['id'])['states'][0] + # + # if not gs['on_first'] and not gs['on_second'] and not gs['on_third']: + # await ctx.send('I don\'t see any baserunners aboard - maybe there wasn\'t an advance.') + # return + # + # runner_found = None + # runner_player = None + # this_runner = None + # base_taken = True + # count = 4 + # for runner in [gs['on_third'], gs['on_second'], gs['on_first']]: + # count -= 1 + # if runner: + # runner_player = await get_one_player(runner['player_id']) + # prompt = f'Did {runner_player["name"]} attempt the advance?' + # this_q = Question(self.bot, ctx.channel, prompt, 'yesno', 10) + # resp = await this_q.ask([ctx.author]) + # if resp: + # runner_found = count + # this_runner = runner + # break + # + # if not runner_found: + # await ctx.send('If nobody made an attempt - it must not have been an xba.') + # return + # + # prompt = 'Was there a throw from an outfielder? (Was a d20 rolled?)' + # this_q = Question(self.bot, ctx.channel, prompt, 'yesno', 10) + # defender_found = None + # this_defender = None + # resp = await this_q.ask([ctx.author]) + # if resp: + # count = -1 + # for defender in [self.get_defender(this_game, 'CF'), self.get_defender(this_game, 'RF'), + # self.get_defender(this_game, 'LF')]: + # count += 1 + # if defender: + # a_defender = await get_one_player(defender['player_id']) + # prompt = f'Did {a_defender["name"]} make the throw?' + # this_q = Question(self.bot, ctx.channel, prompt, 'yesno', 10) + # resp = await this_q.ask([ctx.author]) + # if resp: + # defender_found = ['CF', 'RF', 'LF'][count] + # this_defender = defender + # break + # + # if not defender_found: + # await ctx.send('So I guess there wasn\'t an attempt.') + # return + # + # this_q.prompt = f'Did {runner_player["name"]} reach safely?' + # resp = await this_q.ask([ctx.author]) + # if resp is not None: + # base_taken = resp + # + # logging.info(f'defender: {defender}') + # post_defense({ + # 'game_id': this_game['id'], + # 'player_id': this_defender['player_id'], + # 'position': this_defender['position'], + # 'runner_adv_att': 1, + # 'runner_throw_out': not base_taken + # }) + # + # post_running({ + # 'game_id': this_game['id'], + # 'runner_id': this_runner['player_id'], + # 'extra_base_attempt': 1, + # 'extra_base_taken': base_taken + # }) + # + # if base_taken: + # if runner_found == 1: + # self.adv_specific_runner(this_game, 1, 1) + # elif runner_found == 2: + # self.adv_specific_runner(this_game, 2, 1) + # else: + # self.adv_specific_runner(this_game, 3, 1) + # self.increment_score(this_game, 1) + # await ctx.send('**Take a note to log this RBI - it\'s one of the things ' + # 'I can\'t do automatically, yet.**') + # else: + # if runner_found == 1: + # patch_game_state( + # state_id=gs['id'], + # on_first_id=False + # ) + # elif runner_found == 2: + # patch_game_state( + # state_id=gs['id'], + # on_second_id=False + # ) + # else: + # patch_game_state( + # state_id=gs['id'], + # on_third_id=False + # ) + # self.increment_outs(this_game, 1) + # + # await ctx.send(content=None, embed=await self.game_state_embed(this_game, full_length=False)) + + # @log_play.command(name='robs', aliases=['roba'], help='RobA, RobS') + # @commands.has_any_role(SBA_PLAYERS_ROLE_NAME) + # async def log_rob_command(self, ctx): + # this_game = get_game(channel_id=ctx.channel.id, active=1) + # + # defender_found = None + # this_defender = None + # count = 0 + # for defender in [self.get_defender(this_game, 'CF'), self.get_defender(this_game, 'LF'), + # self.get_defender(this_game, 'RF')]: + # count += 1 + # if defender: + # a_defender = await get_one_player(defender['player_id']) + # prompt = f'Did {a_defender["name"]} attempt the rob?' + # this_q = Question(self.bot, ctx.channel, prompt, 'yesno', 10) + # resp = await this_q.ask([ctx.author]) + # if resp: + # defender_found = True + # this_defender = defender + # break + # + # if not defender_found: + # await ctx.send('So I guess there wasn\'t an attempt.') + # return + # + # prompt = f'Did {a_defender["name"]} successfully rob it?' + # this_q = Question(self.bot, ctx.channel, prompt, 'yesno', 10) + # resp = await this_q.ask([ctx.author]) + # if resp is not None: + # base_taken = resp + # + # post_defense({ + # 'game_id': this_game['id'], + # 'player_id': this_defender['player_id'], + # 'position': this_defender['position'], + # 'rob_attempt': 1, + # 'rob_success': resp + # }) + # + # await ctx.send(content=None, embed=await self.game_state_embed(this_game, full_length=False)) + + @log_play.command( + name='out', + aliases=['flyout', 'groundout', 'popout', 'lineout', 'strikeout', 'fo', 'bpfo', 'go', 'po', 'lo', 'bplo', + 'so', 'k'], + help='fo, bpfo, go, po, lo, bplo, so, k' + ) + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME) + async def log_out_command(self, ctx): + this_game = get_game(channel_id=ctx.channel.id, active=1) + + cmd = ctx.message.content.lower() + if 'bpfo' in cmd: + data = self.log_play_core(this_game, outs=1, ab=1) + sac_rbi = self.adv_specific_runner(this_game, 3, 1) + patch_atbat( + atbat_id=data['ab']['id'], + ab=1 - sac_rbi, + bpfo=1, + rbi=sac_rbi, + sac=sac_rbi + ) + else: + data = self.log_play_core(this_game, outs=1) + if 'so' in cmd or 'k' in cmd or 'strikeout' in cmd: + patch_atbat( + atbat_id=data['ab']['id'], + so=1 + ) + elif 'bplo' in cmd: + patch_atbat( + atbat_id=data['ab']['id'], + bplo=1 + ) + + await ctx.send(content=None, embed=await self.game_state_embed(this_game, full_length=False)) + + @log_play.command(name='xcheck', aliases=['x'], help='x ') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def log_xcheck_command(self, ctx, position): + this_game = get_game(channel_id=ctx.channel.id, active=1) + if position.upper() not in ['C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'P']: + await ctx.send(f'Ope, **{position.upper()}** is not a valid position.') + return + + defender = self.get_defender(this_game, position) + this_state = get_game_state(game_id=this_game['id'])['states'][0] + + prompt = f'Please enter the hit/out result (\'1B(\\*\\*)\', \'2B(\\*\\*\\*)\', \'3B\', or \'out\')' + this_q = Question(self.bot, ctx.channel, prompt, 'text', 15) + resp = await this_q.ask([ctx.author]) + if not resp: + await ctx.send('You think on it and get back to me.') + return + elif resp.upper() not in ['1B', '1BWH', '1B**', '2B', '2BWH', '2B***', '3B', 'OUT']: + await ctx.send('I can only take 1B, 2B, 3B, or OUT') + return + else: + if resp.upper() == 'OUT': + hit = 0 + else: + hit_bases = int(resp[0]) + if len(resp) > 2: + bases = int(resp[0]) + 1 + else: + bases = hit_bases + hit = 1 + data = self.log_play_core(this_game, bases=bases, hit=1) + if hit_bases == 1: + patch_game_state( + state_id=this_state['id'], + on_first_id=data['batter']['id'] + ) + elif hit_bases == 2: + patch_atbat( + data['ab']['id'], + double=1, + ) + patch_game_state( + state_id=this_state['id'], + on_second_id=data['batter']['id'] + ) + elif hit_bases == 3: + patch_atbat( + data['ab']['id'], + triple=1, + ) + patch_game_state( + state_id=this_state['id'], + on_third_id=data['batter']['id'] + ) + + prompt = f'For an error, please enter the number of bases. Otherwise, enter 0.' + this_q.prompt = prompt + this_q.qtype = 'int' + resp = await this_q.ask([ctx.author]) + if resp is None: + await ctx.send('You think on it and get back to me.') + return + elif 0 > resp > 3: + await ctx.send('I can only take 0 -> 3') + return + else: + if resp > 0: + error = 1 + if hit == 0: + data = self.log_play_core(this_game, bases=resp, error=1) + logging.info(f'error: {resp}') + if resp == 1: + patch_game_state( + state_id=this_state['id'], + on_first_id=data['batter']['id'] + ) + elif resp == 2: + patch_game_state( + state_id=this_state['id'], + on_second_id=data['batter']['id'] + ) + elif resp == 3: + patch_game_state( + state_id=this_state['id'], + on_third_id=data['batter']['id'] + ) + else: + self.advance_runners(this_state=this_state, num_bases=resp, error=1) + else: + error = 0 + + post_defense({ + 'game_id': this_game['id'], + 'player_id': defender['player_id'], + 'x_check': 1, + 'position': defender['position'], + 'hit': hit, + 'error': error + }) + if hit == 0 and error == 0: + await ctx.send('**The defense check has been logged - please log the specific out now. (e.g. !log ' + 'fo, !log gora, !log sacf)**') + + await ctx.send(content=None, embed=await self.game_state_embed(this_game, full_length=False)) + + @log_play.command(name='doubleplay', aliases=['gidp', 'gba'], help='gidp, gbA') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def log_gidp_command(self, ctx): + this_game = get_game(channel_id=ctx.channel.id, active=1) + gs = get_game_state(game_id=this_game['id'])['states'][0] + + if not gs['on_first'] and not gs['on_second'] and not gs['on_third']: + await ctx.send('I don\'t see any baserunners aboard - maybe this should just be a groundout.') + return + + runner_found = None + count = 0 + for runner in [gs['on_first'], gs['on_second'], gs['on_third']]: + count += 1 + if runner: + this_runner = await get_one_player(runner['player_id']) + prompt = f'Was {this_runner["name"]} doubled off?' + this_q = Question(self.bot, ctx.channel, prompt, 'yesno', 10) + resp = await this_q.ask([ctx.author]) + if resp: + runner_found = count + break + + if not runner_found: + await ctx.send('If nobody was double off - it must not have been a GIDP.') + return + + # If both outs of gidp are recorded, remove baserunner and record gidp + if gs['outs'] < 2: + gidp = 1 + if runner_found == 1: + patch_game_state( + state_id=gs['id'], + on_first_id=False + ) + elif runner_found == 2: + patch_game_state( + state_id=gs['id'], + on_second_id=False + ) + else: + patch_game_state( + state_id=gs['id'], + on_third_id=False + ) + else: + gidp = 0 + + data = self.log_play_core(this_game, outs=2) + patch_atbat( + atbat_id=data['ab']['id'], + gidp=gidp + ) + + gs = get_game_state(game_id=this_game['id'])['states'][0] + rbi = 0 + count = 1 + for runner in [gs['on_second'], gs['on_third']]: + count += 1 + if runner: + this_runner = await get_one_player(runner['player_id']) + prompt = f'Did {this_runner["name"]} advance?' + this_q = Question(self.bot, ctx.channel, prompt, 'yesno', 10) + resp = await this_q.ask([ctx.author]) + if resp: + rbi += self.adv_specific_runner(this_game, count, 1) + + if rbi: + patch_atbat( + data['ab']['id'], + rbi=rbi, + ) + patch_game_state( + gs['id'], + home_score=gs['home_score'] + rbi + ) + + await ctx.send(content=None, embed=await self.game_state_embed(this_game, full_length=False)) + + @log_play.command(name='gora', aliases=['gbc'], help='gora, gbC') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def log_gora_command(self, ctx): + this_game = get_game(channel_id=ctx.channel.id, active=1) + + data = self.log_play_core(this_game, outs=1, bases=1) + + await ctx.send(content=None, embed=await self.game_state_embed(this_game, full_length=False)) + + @log_play.command(name='fc', aliases=['gbb'], help='fc, gbB') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def log_fc_command(self, ctx): + this_game = get_game(channel_id=ctx.channel.id, active=1) + gs = get_game_state(game_id=this_game['id'])['states'][0] + + if not gs['on_first'] and not gs['on_second'] and not gs['on_third']: + await ctx.send('I don\'t see any baserunners aboard - maybe this should just be a groundout.') + return + + runner_found = None + count = 0 + for runner in [gs['on_first'], gs['on_second'], gs['on_third']]: + count += 1 + if runner: + this_runner = await get_one_player(runner['player_id']) + prompt = f'Was {this_runner["name"]} the forced out runner?' + this_q = Question(self.bot, ctx.channel, prompt, 'yesno', 10) + resp = await this_q.ask([ctx.author]) + if resp: + runner_found = count + break + + if not runner_found: + await ctx.send('If nobody was forced out - it must not have been a fielder\'s choice.') + return + + # If fewer than 2 outs, remove forced out runner first + if gs['outs'] < 2: + if runner_found == 1: + patch_game_state( + state_id=gs['id'], + on_first_id=False + ) + elif runner_found == 2: + patch_game_state( + state_id=gs['id'], + on_second_id=False + ) + else: + patch_game_state( + state_id=gs['id'], + on_third_id=False + ) + + data = self.log_play_core(this_game, outs=1) + patch_game_state( + gs['id'], + on_first_id=data['batter']['id'] + ) + + gs = get_game_state(game_id=this_game['id'])['states'][0] + rbi = 0 + count = 1 + for runner in [gs['on_second'], gs['on_third']]: + count += 1 + if runner: + this_runner = await get_one_player(runner['player_id']) + prompt = f'Did {this_runner["name"]} advance?' + this_q = Question(self.bot, ctx.channel, prompt, 'yesno', 10) + resp = await this_q.ask([ctx.author]) + if resp: + rbi += self.adv_specific_runner(this_game, count, 1) + + if rbi: + patch_atbat( + data['ab']['id'], + rbi=rbi, + ) + self.increment_score(this_game, rbi) + # patch_game_state( + # gs['id'], + # home_score=gs['home_score'] + rbi + # ) + + await ctx.send(content=None, embed=await self.game_state_embed(this_game, full_length=False)) + + @log_play.command(name='chaos', aliases=['wp', 'pb', 'pick', 'balk'], help='wp, pb, pick, balk') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def log_chaos_command(self, ctx): + this_game = get_game(channel_id=ctx.channel.id, active=1) + gs = get_game_state(game_id=this_game['id'])['states'][0] + + cmd = ctx.message.content.lower() + pick = 0 + wp = 0 + pb = 0 + balk = 0 + if 'pick' in cmd: + if not gs['on_first'] and not gs['on_second'] and not gs['on_third']: + await ctx.send('I don\'t see any baserunners aboard.') + return + + runner_found = None + count = 0 + for runner in [gs['on_first'], gs['on_second'], gs['on_third']]: + count += 1 + if runner: + this_runner = await get_one_player(runner['player_id']) + prompt = f'Was {this_runner["name"]} picked off?' + this_q = Question(self.bot, ctx.channel, prompt, 'yesno', 10) + resp = await this_q.ask([ctx.author]) + if resp: + runner_found = count + break + + if not runner_found: + await ctx.send('If nobody was picked off - maybe try another play.') + return + elif runner_found == 1: + patch_game_state( + gs['id'], + on_first_id=False + ) + elif runner_found == 2: + patch_game_state( + gs['id'], + on_second_id=False + ) + elif runner_found == 3: + patch_game_state( + gs['id'], + on_third_id=False + ) + + pick = 1 + self.increment_outs(this_game, 1) + else: + if 'wp' in cmd: + wp = 1 + elif 'balk' in cmd: + balk = 1 + else: + pb = 1 + runs = self.advance_runners(gs, num_bases=1, error=True) + if runs: + self.increment_score(this_game, runs) + + data = self.get_pbc(this_game) + + post_chaos({ + 'game_id': this_game['id'], + 'pitcher_id': data['pitcher']['player_id'], + 'catcher_id': data['catcher']['player_id'], + 'wild_pitch': wp, + 'passed_ball': pb, + 'pick_off': pick, + 'balk': balk + }) + + await ctx.send(content=None, embed=await self.game_state_embed(this_game, full_length=False)) + + @commands.command(name='undo', help='Roll back last entered play', hidden=True) + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def roll_back_command(self, ctx): + await ctx.send('This command will do something soon, I promise.') + + @commands.command(name='advance', aliases=['adv'], help='!adv ') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def advance_runners_command(self, ctx, from_base: int, num_bases: int): + this_game = get_game(channel_id=ctx.channel.id, active=1) + if 0 > from_base > 3: + await ctx.send('from_base parameter must be 1, 2, or 3') + return + if 0 > num_bases > 3: + await ctx.send('num_bases parameter must be 1, 2, or 3') + return + + self.adv_specific_runner(this_game, from_base, num_bases) + await ctx.send(content=None, embed=await self.game_state_embed(this_game, full_length=False)) + + @commands.command(name='place', help='!place ') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def place_runner_command(self, ctx, base: int, *, player_name): + this_game = get_game(channel_id=ctx.channel.id, active=1) + gs = get_game_state(game_id=this_game['id'])['states'][0] + if 0 > base > 3: + await ctx.send('from_base parameter must be 1, 2, or 3') + return + + player_cog = self.bot.get_cog('Players') + player_name = await fuzzy_player_search(ctx, ctx.channel, self.bot, player_name, player_cog.player_list.keys()) + player = await get_one_player(player_name) + this_member = get_one_member( + game_id=this_game['id'], + player_id=player['id'], + active=1 + ) + + if base == 1: + patch_game_state( + state_id=gs['id'], + on_first_id=this_member['id'] + ) + elif base == 2: + patch_game_state( + state_id=gs['id'], + on_second_id=this_member['id'] + ) + else: + patch_game_state( + state_id=gs['id'], + on_third_id=this_member['id'] + ) + await ctx.send(content=None, embed=await self.game_state_embed(this_game, full_length=False)) + + @commands.command(name='clear', help='!clear ') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def clear_runner_command(self, ctx, base: int): + this_game = get_game(channel_id=ctx.channel.id, active=1) + gs = get_game_state(game_id=this_game['id'])['states'][0] + if 0 > base > 3: + await ctx.send('from_base parameter must be 1, 2, or 3') + return + + if base == 1: + patch_game_state( + state_id=gs['id'], + on_first_id=False + ) + elif base == 2: + patch_game_state( + state_id=gs['id'], + on_second_id=False + ) + else: + patch_game_state( + state_id=gs['id'], + on_third_id=False + ) + await ctx.send(content=None, embed=await self.game_state_embed(this_game, full_length=False)) + + @commands.command(name='outs', help='!outs ') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def outs_command(self, ctx, modifier: int): + this_game = get_game(channel_id=ctx.channel.id, active=1) + this_state = get_game_state(game_id=this_game['id'])['states'][0] + + if -2 <= modifier <= 3: + outs = modifier + else: + await ctx.send('I can only change the outs from -2 to +3') + return + if outs == 0: + await ctx.message.add_reaction('👍') + return + + if outs > 0: + self.increment_outs(this_game, outs) + else: + if this_state['outs'] + outs < 0: + new_outs = 0 + else: + new_outs = this_state['outs'] + outs + patch_game_state( + this_state['id'], + outs=0 + ) + + await ctx.send(content=None, embed=await self.game_state_embed(this_game, full_length=False)) + + @commands.command(name='score', help='!score ') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def score_command(self, ctx, home_or_away: str, modifier: int): + this_game = get_game(channel_id=ctx.channel.id, active=1) + this_state = get_game_state(game_id=this_game['id'])['states'][0] + home_score = this_state['home_score'] + away_score = this_state['away_score'] + + if home_or_away.lower() == 'home': + new_home_score = home_score + modifier + new_away_score = away_score + elif home_or_away.lower() == 'away': + new_away_score = away_score + modifier + new_home_score = home_score + else: + await ctx.send('I only take \'home\' or \'away\'. Try again.') + return + + new_state = patch_game_state( + this_state['id'], + home_score=new_home_score, + away_score=new_away_score + ) + + await ctx.send(content=None, embed=await self.game_state_embed(this_game, full_length=False)) + + @commands.group(name='show', help='`!help show` for all options') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def show_data(self, ctx): + if ctx.invoked_subcommand is None: + await ctx.send('No play details listed. Run `!help show` for available commands.') + + @show_data.command(name='pitcher', help='Show pitcher card') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def show_pitcher_command(self, ctx): + this_game = get_game(channel_id=ctx.channel.id, active=1) + gs = get_game_state(game_id=this_game['id'])['states'][0] + logging.info(f'gs: {gs}') + + if gs['top_half']: + this_member = get_one_member( + game_id=this_game['id'], + team_id=this_game['home_team_id'], + batting_order=10, + active=1 + ) + else: + this_member = get_one_member( + game_id=this_game['id'], + team_id=this_game['away_team_id'], + batting_order=10, + active=1 + ) + + this_player = await get_one_player(this_member['player_id']) + await ctx.send(content=None, embed=await get_player_embed(this_player, await get_current())) + + @show_data.command(name='batter', help='Show batter card') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def show_batter_command(self, ctx): + this_game = get_game(channel_id=ctx.channel.id, active=1) + gs = get_game_state(game_id=this_game['id'])['states'][0] + logging.info(f'gs: {gs}') + + if gs['top_half']: + this_member = get_one_member( + game_id=this_game['id'], + team_id=this_game['away_team_id'], + batting_order=gs['away_batter_up'], + active=1 + ) + else: + this_member = get_one_member( + game_id=this_game['id'], + team_id=this_game['home_team_id'], + batting_order=gs['home_batter_up'], + active=1 + ) + + this_player = await get_one_player(this_member['player_id']) + await ctx.send(content=None, embed=await get_player_embed(this_player, await get_current())) + + @show_data.command( + name='position', + aliases=['c', '1b', '2b', '3b', 'ss', 'lf', 'cf', 'rf'], + help='!show 1b, !show rf, etc' + ) + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def show_defense_command(self, ctx): + this_game = get_game(channel_id=ctx.channel.id, active=1) + gs = get_game_state(game_id=this_game['id'])['states'][0] + + pos = ctx.message.content.split(' ')[1].upper() + logging.info(f'pos: {pos}') + + this_player = await get_one_player(self.get_defender(this_game, pos)['player_id']) + await ctx.send(content=None, embed=await get_player_embed(this_player, await get_current())) + + @show_data.command(name='runner', help='!show runner ') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def show_runner_command(self, ctx, base_num: int): + this_game = get_game(channel_id=ctx.channel.id, active=1) + gs = get_game_state(game_id=this_game['id'])['states'][0] + + if base_num == 1: + runner_id = gs['on_first']['player_id'] + elif base_num == 2: + runner_id = gs['on_second']['player_id'] + elif base_num == 3: + runner_id = gs['on_third']['player_id'] + else: + await ctx.send('I can only check for runners on base 1, 2, or 3. Try again maybe?') + return + + this_player = await get_one_player(runner_id) + await ctx.send(content=None, embed=await get_player_embed(this_player, await get_current())) + + @commands.command(name='stats', aliases=['tocsv'], help='Get current stats') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def export_csv_command(self, ctx): + this_game = get_game(channel_id=ctx.channel.id, active=1) + + async with ctx.typing(): + results = await self.get_game_results(this_game, combined=True) + + csv = DataFrame(results['batters']).to_csv(header=False, index=False) + file_name = f'storage/csv/{ctx.author.display_name}{random.randint(1000000000,9999999999)}.csv' + file = open(file_name, 'w') + file.write(csv) + file.close() + + await ctx.send(content='Here is your batter csv:', file=discord.File(file_name)) + + csv = DataFrame(results['pitchers']).to_csv(header=False, index=False) + file_name = f'storage/csv/{ctx.author.display_name}{random.randint(1000000000,9999999999)}.csv' + file = open(file_name, 'w') + file.write(csv) + file.close() + + await ctx.send(content='Here is your pitcher csv:', file=discord.File(file_name)) + + @commands.command(name='tosheets', help='Get results Sheet') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def export_sheets_command(self, ctx): + this_game = get_game(channel_id=ctx.channel.id, active=1) + away_team = await get_one_team(this_game['away_team_id']) + home_team = await get_one_team(this_game['home_team_id']) + + results = await self.get_game_results(this_game) + await ctx.send('You got it - just pulled stats and posting them to a spreadsheet for ya.') + + async with ctx.typing(): + sheet_id = await self.get_game_sheet(home_team, away_team, results, ctx.author.display_name) + + await ctx.send(f'Here is the results sheet for this game: https://docs.google.com/spreadsheets/d/' + f'{sheet_id}') + + @commands.command(name='pull', hidden=True) + @commands.is_owner() + async def pull_command(self, ctx, player_type): + if player_type.lower() not in ['pitchers', 'batters']: + await ctx.send('Can only import pitcher or batter') + return + + # Get data from Sheets + async with ctx.typing(): + sheets = pygsheets.authorize(service_file='storage/major-domo-service-creds.json') + if player_type == 'pitchers': + data_sheet = sheets.open_by_key('1tAKj1PXTJbVyEXzB8YPcGOdydS24N12qoT6n7bSSk_w').worksheet_by_title( + '2020 Pitchers') + raw_data = data_sheet.get_values('A3', 'AP547') + else: + data_sheet = sheets.open_by_key('1tAKj1PXTJbVyEXzB8YPcGOdydS24N12qoT6n7bSSk_w').worksheet_by_title( + '2020 Batters') + raw_data = data_sheet.get_values('A3', 'AZ509') + + await ctx.send('Alrighty, got the import data. Now to sift through...') + + all_data = [] + async with ctx.typing(): + if player_type == 'pitchers': + for line in raw_data: + this_player = await get_one_player(line[0]) + this_line = { + 'player_id': this_player['id'], + 'name': line[0], + 'hand': line[2], + 'innings': line[3], + 'so_vl': line[4], + 'so_vr': line[5], + 'bb_vl': line[6], + 'bb_vr': line[7], + 'hit_vl': line[8], + 'hit_vr': line[9], + 'ob_vl': line[10], + 'ob_vr': line[11], + 'tb_vl': line[12], + 'tb_vr': line[13], + 'tb123_vl': line[16], + 'tb123_vr': line[17], + 'hit123_vl': line[14], + 'hit123_vr': line[15], + 'hr_vl': line[18], + 'hr_vr': line[19], + 'bphr_vl': line[22], + 'bphr_vr': line[23], + 'bp1b_vl': line[24], + 'bp1b_vr': line[25], + 'dp_vl': line[26], + 'dp_vr': line[27], + 'hold': line[29], + 'st_rating': line[30] if line[30] != '' else None, + 're_rating': line[31] if line[31] != '' else None, + 'cl_rating': line[32] if line[32] != '' else None, + 'range': line[33], + 'error': line[34], + 'balk': line[35], + 'wild_pitch': line[36], + 'speed': line[39], + 'bunt': line[40], + 'injury': line[41], + } + all_data.append(this_line) + + posts = post_pitchers(all_data) + else: + for line in raw_data: + this_player = await get_one_player(line[0]) + this_line = { + 'player_id': this_player['id'], + 'name': line[0], + 'hand': line[3], + 'atbats': line[2], + 'so_vl': line[4], + 'so_vr': line[5], + 'bb_vl': line[6], + 'bb_vr': line[7], + 'hit_vl': line[8], + 'hit_vr': line[9], + 'ob_vl': line[10], + 'ob_vr': line[11], + 'tb_vl': line[12], + 'tb_vr': line[13], + 'hr_vl': line[14], + 'hr_vr': line[15], + 'bphr_vl': line[18], + 'bphr_vr': line[19], + 'bp1b_vl': line[20], + 'bp1b_vr': line[21], + 'dp_vl': line[24], + 'dp_vr': line[25], + 'steal': line[27], + 'speed': line[28], + 'bunt': line[29], + 'hit_and_run': line[30], + 'injury': line[31], + 'rating_c': line[32] if line[32] != '' else None, + 'rating_1b': line[33] if line[33] != '' else None, + 'rating_2b': line[34] if line[34] != '' else None, + 'rating_3b': line[35] if line[35] != '' else None, + 'rating_ss': line[36] if line[36] != '' else None, + 'rating_lf': line[37] if line[37] != '' else None, + 'rating_cf': line[38] if line[38] != '' else None, + 'rating_rf': line[39] if line[39] != '' else None, + 'error_c': line[40] if line[40] != '' else None, + 'error_1b': line[41] if line[41] != '' else None, + 'error_2b': line[42] if line[42] != '' else None, + 'error_3b': line[43] if line[43] != '' else None, + 'error_ss': line[44] if line[44] != '' else None, + 'error_lf': line[45] if line[45] != '' else None, + 'error_cf': line[46] if line[46] != '' else None, + 'error_rf': line[47] if line[47] != '' else None, + 'arm_c': line[49] if line[49] != '' else None, + 'arm_of': line[48] if line[48] != '' else None, + 'overthrow': line[50] if line[50] != '' else None, + 'passed_ball': line[51] if line[51] != '' else None, + } + all_data.append(this_line) + posts = post_batters(all_data) + + await ctx.send(f'Posted {posts} {player_type.lower()}!') + + @commands.command(name='vs', help='!vs vs ') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def versus_command(self, ctx, *, args): + # Split names out + names = re.split(' vs ', args) + if len(names) == 1: + names = re.split(' vs. ', args) + if len(names) == 1: + await ctx.send('I could not find player names in that message. Make sure the format is ' + '`!vs Gerrit Cole vs Salvador Perez') + return + + # Perform fuzzy search to get player objects + player_cog = self.bot.get_cog('Players') + player_name = await fuzzy_player_search(ctx, ctx.channel, self.bot, names[0], player_cog.player_list.keys()) + player1 = await get_one_player(player_name) + player_name = await fuzzy_player_search(ctx, ctx.channel, self.bot, names[1], player_cog.player_list.keys()) + player2 = await get_one_player(player_name) + pitcher_player = None + batter_player = None + pitcher_pos = ['SP', 'RP', 'CP'] + + if player1['pos_1'] in pitcher_pos: + pitcher_player = player1 + if player2['pos_1'] not in pitcher_pos: + batter_player = player2 + else: + await ctx.send(f'I could not find batting info for {player2["name"]}.') + return + else: + pitcher_player = player2 + if player1['pos_1'] not in pitcher_pos: + batter_player = player1 + else: + await ctx.send(f'I could not find batting info for {player1["name"]}.') + return + + # Get Pitcher and Batter objects + pitcher = get_one_pitcher(pitcher_player['id']) + batter = get_one_batter(batter_player['id']) + + da_roll = random.randint(1, 1000) + embed = get_team_embed(f'{pitcher_player["name"]} vs {batter_player["name"]}', thumbnail=False) + + # Calculate matchup OBP + batter_ob = batter['ob_vr'] if pitcher['hand'] == 'R' else batter['ob_vl'] + if batter['hand'] == 'L': + pitcher_ob = pitcher['ob_vl'] + elif batter['hand'] == 'R': + pitcher_ob = pitcher['ob_vr'] + else: + pitcher_ob = pitcher['ob_vr'] if pitcher['hand'] == 'L' else pitcher['ob_vl'] + obp = ((batter_ob + pitcher_ob) / 216) + + # Calculate matchup AVG + batter_hit = batter['hit_vr'] if pitcher['hand'] == 'R' else batter['hit_vl'] + batter_hr = batter['hr_vr'] if pitcher['hand'] == 'R' else batter['hr_vl'] + batter_bb = batter['bb_vr'] if pitcher['hand'] == 'R' else batter['bb_vl'] + batter_so = batter['so_vr'] if pitcher['hand'] == 'R' else batter['so_vl'] + if batter['hand'] == 'L': + pitcher_hit = pitcher['hit_vl'] + pitcher_hr = pitcher['hr_vl'] + pitcher_bb = pitcher['bb_vl'] + pitcher_so = pitcher['so_vl'] + elif batter['hand'] == 'R': + pitcher_hit = pitcher['hit_vr'] + pitcher_hr = pitcher['hr_vr'] + pitcher_bb = pitcher['bb_vr'] + pitcher_so = pitcher['so_vr'] + else: + pitcher_hit = pitcher['hit_vr'] if pitcher['hand'] == 'L' else pitcher['hit_vl'] + pitcher_hr = pitcher['hr_vr'] if pitcher['hand'] == 'L' else pitcher['hr_vl'] + pitcher_bb = pitcher['bb_vr'] if pitcher['hand'] == 'L' else pitcher['bb_vl'] + pitcher_so = pitcher['so_vr'] if pitcher['hand'] == 'L' else pitcher['so_vl'] + avg = ((batter_hit + pitcher_hit) / (216 - batter_ob - pitcher_ob + batter_hit + pitcher_hit)) + + # Calculate matchup SLG + batter_tb = batter['tb_vr'] if pitcher['hand'] == 'R' else batter['tb_vl'] + if batter['hand'] == 'L': + pitcher_tb = pitcher['tb_vl'] + elif batter['hand'] == 'R': + pitcher_tb = pitcher['tb_vr'] + else: + pitcher_tb = pitcher['tb_vr'] if pitcher['hand'] == 'L' else pitcher['tb_vl'] + slg = ((batter_tb + pitcher_tb) / (216 - batter_ob - pitcher_ob + batter_hit + pitcher_hit)) + + embed.add_field(name='Average', value=f'{avg:.3f}') + embed.add_field(name='On Base', value=f'{obp:.3f}') + embed.add_field(name='Slugging', value=f'{slg:.3f}') + + # Check for hit + if da_roll <= round(avg * 1000): + hit_roll = random.randint(1, round((batter_hit + pitcher_hit) * 1000)) + if hit_roll <= round((batter_hr + pitcher_hr) * 1000): + result = 'Home Run!' + else: + tb123 = batter_tb + pitcher_tb - ((batter_hr + pitcher_hr) * 4) + si_chance = tb123 / (batter_hit + pitcher_hit) + if random.random() <= si_chance: + result = 'Single!' + else: + result = 'Double!' + + # Check for on base + elif da_roll <= round(obp * 1000): + bb_chance = (batter_bb + pitcher_bb) / (batter_ob + pitcher_ob - batter_hit - pitcher_hit) + logging.info(f'bb_chance: {bb_chance}') + if random.random() <= bb_chance: + result = 'Walk!' + else: + result = 'HBP!' + + # Check for out + else: + out_chances = 216 - round(216 * obp) + out_roll = random.randint(1, out_chances) + + if out_roll <= batter_so + pitcher_so: + result = 'Strikeout!' + elif out_roll <= out_chances - ((out_chances - (batter_so + pitcher_so)) / 2): + result = 'Groundout!' + else: + result = 'Flyout!' + + embed.add_field(name='Result', value=result) + + logging.info(f'{pitcher_player["name"]} vs {batter_player["name"]}\n' + f'batter OB: {batter_ob} / pitcher OB: {pitcher_ob}\n' + f'batter BB: {batter_bb} / pitcher BB: {pitcher_bb}\n' + f'batter hit: {batter_hit} / pitcher hit: {pitcher_hit}\n' + f'batter hr: {batter_hr} / pitcher hr: {pitcher_hr}') + await ctx.send(content=None, embed=embed) + + @commands.command(name='tutorial', help='Quick how-to guide') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def tutorial_command(self, ctx): + embed = get_team_embed(f'Discord Scorebot Tutorial') + embed.add_field( + name='Start & End Game', + value=f'The `!startgame` command will initiate a game in the channel it is run. Other commands will only ' + f'be accepted in this channel. The `!endgame` command will ask for confirmation and then send game ' + f'stats to a google sheet (just like running `!tosheets`) before closing out the game.' + ) + embed.add_field( + name='Set Lineups', + value=f'The `!setlineups` command or `!sl` is used to set the initial lineups as well as make mid-game ' + f'substitutions. The format is as follows:\n```!sl --' + f'```\nIf there are multiple subs (or this is the initial lineup), enter a comma after the ' + f'position and, without any spaces, begin the next player. Below are two examples:\n```!sl WV 1-' + f'Ronald Acuna Jr-RF```and```!sl WV 2-Trea Turner-SS,4-Eloy Jimenez-RF,7-Cody Bellinger-CF,10-' + f'Anthony Bass-P```\nNote on pitchers: if the pitcher is not batting, they are entered with 10 for ' + f'their lineup_order; if they enter the game to bat, they are then entered in their batting order.', + inline=False + ) + embed.add_field( + name='Logging Plays', + value=f'Plays are logged with the `!log ` command. You can run `!help log` to see a full ' + f'list. For help with a specific play log, run `!help log x` for help logging an x-chance.', + inline=False + ) + embed.add_field( + name='Game State Modifications', + value=f'The score, the number of ours, and the placement of baserunners can all be modified manually in ' + f'cases of either bad automation (please let Cal know) or a mistake was made.\nThe `!advance` ' + f'command manually advanced baserunners; the `!clear` command removes a baserunner; the `!outs` ' + f'command can add or subtract outs (never past 0 or 3); the `!place` command manually places ' + f'baserunners; the `!score` command adjusts either the home or away team\'s score.', + inline=False + ) + embed.add_field( + name='Results', + value=f'The `!tosheets` command will compile the game\'s stats and upload them into a google sheets page ' + f'formatted for the !submit command. The `!stats` command will display the game\'s stats in a simple ' + f'csv format and post them to discord. Both of these commands can be run at any time __before__ the ' + f'`!endgame` command is run.', + inline=False + ) + await ctx.send(content=None, embed=embed) + + +async def setup(bot): + await bot.add_cog(Gameday(bot)) + diff --git a/cogs/gameplay.py b/cogs/gameplay.py new file mode 100644 index 0000000..fd11faf --- /dev/null +++ b/cogs/gameplay.py @@ -0,0 +1,1256 @@ +import math + +import discord +from discord import app_commands +from discord.ext import commands +from peewee import IntegrityError + +from typing import Optional, Literal +import logging + +from helpers import SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME, random_conf_gif, SBA_SEASON, PD_SEASON, LOGO, \ + get_team_embed, Confirm, get_pos_abbrev, SBA_COLOR +from gameplay_helpers import * +from db_calls import get_one_team, get_one_player +from db_calls_gameplay import StratGame, StratPlay, StratLineup, get_one_game, pd_get_one_team, post_game, patch_game, \ + get_game_team, post_lineups, pd_get_card_by_id, make_sub, get_player, player_link, get_team_lineups, \ + get_current_play, post_play, get_one_lineup, advance_runners, patch_play, complete_play, get_batting_stats, \ + get_pitching_stats, undo_play, get_latest_play, advance_one_runner, get_play_by_num + + +class Gameplay(commands.Cog): + def __init__(self, bot): + self.bot = bot + + async def cog_command_error(self, ctx, error): + await ctx.send(f'{error}\n\nRun !help to see the command requirements') + + async def slash_error(self, ctx, error): + await ctx.send(f'{error}') + + def get_scorebug(self, home_team, away_team, curr_play): + occupied = '●' + unoccupied = '○' + + first_base = unoccupied if not curr_play.on_first else occupied + second_base = unoccupied if not curr_play.on_second else occupied + third_base = unoccupied if not curr_play.on_third else occupied + half = '▲' if curr_play.inning_half == 'Top' else '▼' + inning = f'{half} {curr_play.inning_num}' + outs = f'{curr_play.starting_outs} Out{"s" if curr_play.starting_outs != 1 else ""}' + + game_string = f'```\n' \ + f'{away_team["abbrev"]: ^4}{curr_play.away_score: ^3} {second_base}' \ + f'{inning: >10}\n' \ + f'{home_team["abbrev"]: ^4}{curr_play.home_score: ^3} {third_base} {first_base}' \ + f'{outs: >8}\n```' + + return game_string + + async def get_game_state(self, game: StratGame) -> dict: + curr_play = get_current_play(game.id) + if curr_play is None: + latest_play = get_latest_play(game.id) + if not latest_play: + away_lineup = await get_team_lineups(game.id, game.away_team_id) + home_lineup = await get_team_lineups(game.id, game.home_team_id) + + if len(away_lineup) == 0 or len(home_lineup) == 0: + game_state = { + 'error': True, + 'away_lineup': away_lineup, + 'home_lineup': home_lineup + } + return game_state + else: + curr_play = post_play({ + 'game_id': game.id, + 'play_num': 1, + 'batter_id': get_one_lineup(game.id, game.away_team_id, batting_order=1).id, + 'pitcher_id': get_one_lineup(game.id, game.home_team_id, position='P').id, + 'on_base_code': 0, + 'inning_half': 'Top', + 'inning_num': 1, + 'batting_order': 1, + 'starting_outs': 0, + 'away_score': 0, + 'home_score': 0 + }) + else: + patch_play(latest_play.id, complete=False) + curr_play = latest_play + + game_state = {'error': False, 'curr_play': curr_play} + + away_team = await get_game_team(game, team_id=game.away_team_id) + home_team = await get_game_team(game, team_id=game.home_team_id) + game_state['away_team'] = away_team + game_state['home_team'] = home_team + + scorebug = self.get_scorebug(home_team, away_team, game_state['curr_play']) + game_state['scorebug'] = scorebug + + batter = await get_player(game, game_state['curr_play'].batter) + litmus = 0 + try: + pitcher = await get_player(game, game_state['curr_play'].pitcher) + litmus = 1 + catcher = await get_player( + game, + get_one_lineup( + game.id, + team_id=game.away_team_id if game_state['curr_play'].inning_half == 'Top' else game.home_team_id, + position='C' + ) + ) + except Exception as e: + logging.info(f'ERROR: {e} / TYPE: {type(e)}') + away_lineup = await get_team_lineups(game.id, game.away_team_id) + home_lineup = await get_team_lineups(game.id, game.home_team_id) + if litmus == 0: + error_message = f'Please sub in a pitcher to continue' + else: + error_message = f'Please sub in a catcher to continue' + + game_state['error'] = True + game_state['error_message'] = error_message + game_state['away_lineup'] = away_lineup + game_state['home_lineup'] = home_lineup + return game_state + + game_state['batter'] = batter + game_state['pitcher'] = pitcher + game_state['catcher'] = catcher + + return game_state + + async def get_game_state_embed(self, game: StratGame, full_length=True): + game_state = await self.get_game_state(game) + + logging.info(f'game_state: {game_state}') + + embed = get_team_embed( + f'{game_state["away_team"]["sname"]} @ {game_state["home_team"]["sname"]}', + team=game_state['home_team'], + thumbnail=False + ) + if abs(game_state['curr_play'].home_score - game_state['curr_play'].away_score) >= 10: + embed.description = 'Mercy rule in effect' + + if game.is_pd: + embed.set_footer(text=f'PD Season {PD_SEASON}', icon_url=LOGO) + + embed.add_field(name='Game State', value=game_state['scorebug'], inline=False) + + if game_state['error']: + # embed = discord.Embed( + # title='Current Lineups', + # color=int(SBA_COLOR, 16) + # ) + embed.add_field(name='Away Team', + value=game_state['away_lineup'] if len(game_state['away_lineup']) else 'None, yet') + embed.add_field(name='Home Team', + value=game_state['home_lineup'] if len(game_state['home_lineup']) else 'None, yet') + if game_state['error_message']: + embed.set_footer(text=game_state['error_message'], icon_url=LOGO) + + return embed + + pitcher_string = f'{player_link(game, game_state["pitcher"])}' + batter_string = f'{player_link(game, game_state["batter"])}' + + all_bat_stats = get_batting_stats(game.id, lineup_id=game_state['curr_play'].batter.id) + if len(all_bat_stats): + b_s = all_bat_stats[0] + batter_string += f'\n{b_s["pl_hit"]}-{b_s["pl_ab"]}' + for num, stat in [ + (b_s['pl_double'], '2B'), (b_s['pl_triple'], '3B'), (b_s['pl_homerun'], 'HR'), (b_s['pl_rbi'], 'RBI'), + (b_s['pl_bb'], 'BB'), (b_s['pl_hbp'], 'HBP'), (b_s['pl_ibb'], 'IBB'), (b_s['pl_so'], 'K'), + (b_s['pl_gidp'], 'GIDP'), (b_s['pl_bpfo'], 'BPFO'), (b_s['pl_bplo'], 'BPLO') + ]: + if num: + batter_string += f', {num if num > 1 else ""}{" " if num > 1 else ""}{stat}' + + all_pit_stats = get_pitching_stats(game.id, lineup_id=game_state['curr_play'].pitcher.id) + if len(all_pit_stats): + p_s = all_pit_stats[0] + pitcher_string += f'\n{math.floor(p_s["pl_outs"]/3)}.{p_s["pl_outs"] % 3} IP' + for num, stat in [ + (p_s['pl_runs'], 'R'), (p_s['pl_hit'], 'H'), (p_s['pl_homerun'], 'HR'), (p_s['pl_so'], 'K'), + (p_s['pl_bb'], 'BB'), (p_s['pl_hbp'], 'HBP'), (p_s['pl_wild_pitch'], 'WP'), (p_s['pl_balk'], 'BK'), + ]: + if num: + pitcher_string += f', {num} {stat}' + if stat == 'R' and num != p_s['pl_eruns']: + pitcher_string += f', {p_s["pl_eruns"]} ER' + + # embed.add_field(name='Matchup', value=f'Pitcher: \nvs\nBatter: ', inline=False) + embed.add_field(name='Pitcher', value=f'{pitcher_string}') + embed.add_field(name='Batter', value=f'{batter_string}') + embed.set_thumbnail(url=game_state["pitcher"]["image"]) + embed.set_image(url=game_state["batter"]["image"]) + + baserunner_string = '' + if game_state['curr_play'].on_first: + runner = await get_player(game, game_state['curr_play'].on_first) + baserunner_string += f'On First: {player_link(game, runner)}\n' + if game_state['curr_play'].on_second: + runner = await get_player(game, game_state['curr_play'].on_second) + baserunner_string += f'On Second: {player_link(game, runner)}\n' + if game_state['curr_play'].on_third: + runner = await get_player(game, game_state['curr_play'].on_third) + baserunner_string += f'On Third: {player_link(game, runner)}\n' + embed.add_field( + name='Baserunners', value=baserunner_string if len(baserunner_string) > 0 else 'None', inline=False + ) + + if not full_length: + return embed + + away_lineup = await get_team_lineups(game.id, game.away_team_id) + home_lineup = await get_team_lineups(game.id, game.home_team_id) + embed.add_field(name=f'{game_state["away_team"]["abbrev"]} Lineup', value=away_lineup) + embed.add_field(name=f'{game_state["home_team"]["abbrev"]} Lineup', value=home_lineup) + return embed + + @commands.hybrid_command(name='newgame', help='Start a new baseball game') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def new_game_command( + self, ctx: commands.Context, away_team_abbrev: str, home_team_abbrev: str, week_num: int, + game_num: Optional[int] = None, is_pd: Optional[bool] = False): + conflict = get_one_game(channel_id=ctx.channel.id, active=True) + if conflict: + await ctx.send(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 + + if is_pd: + away_team = pd_get_one_team(away_team_abbrev) + home_team = pd_get_one_team(home_team_abbrev) + if ctx.author.id not in [away_team['gmid'], home_team['gmid']]: + await ctx.send('You can only start a new game if you GM one of the teams.') + return + else: + away_team = await get_one_team(away_team_abbrev) + home_team = await get_one_team(home_team_abbrev) + if ctx.author.id not in [away_team['gmid'], away_team['gmid2'], home_team['gmid'], home_team['gmid2']]: + await ctx.send('You can only start a new game if you GM one of the teams.') + return + + post_game({ + 'away_team_id': away_team['id'], + 'home_team_id': home_team['id'], + 'week_num': week_num, + 'game_num': game_num, + 'channel_id': ctx.channel.id, + 'active': True, + 'is_pd': is_pd + }) + + await ctx.send(random_conf_gif()) + + @commands.hybrid_command(name='endgame', help='End game in this channel') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def end_game_command(self, ctx: commands.Context): + this_game = get_one_game(channel_id=ctx.channel.id, active=True) + if not this_game: + await ctx.send(f'Ope, I don\'t see a game in this channel.') + return + + patch_game(this_game.id, active=False) + + # TODO: create result sheet and submit stats + if this_game.is_pd: + # Build the statlines + # Submit the scorecard + # Post a notification to PD for rewards + pass + else: + # Send stats to sheets + # Post sheets link + pass + + await ctx.send(random_conf_gif()) + + @app_commands.command( + name='setlineup', + description='Set starting lineup for game in this channel; Player Name for SBa games / Card ID for PD games', + ) + @app_commands.describe( + order_1_player='SBa: Player Name; PD: Card #', + order_1_pos='C for Catcher, 1B for first base, P for pitcher, etc.', + order_2_player='SBa: Player Name; PD: Card #', + order_2_pos='C for Catcher, 1B for first base, P for pitcher, etc.', + order_3_player='SBa: Player Name; PD: Card #', + order_3_pos='C for Catcher, 1B for first base, P for pitcher, etc.', + order_4_player='SBa: Player Name; PD: Card #', + order_4_pos='C for Catcher, 1B for first base, P for pitcher, etc.', + order_5_player='SBa: Player Name; PD: Card #', + order_5_pos='C for Catcher, 1B for first base, P for pitcher, etc.', + order_6_player='SBa: Player Name; PD: Card #', + order_6_pos='C for Catcher, 1B for first base, P for pitcher, etc.', + order_7_player='SBa: Player Name; PD: Card #', + order_7_pos='C for Catcher, 1B for first base, P for pitcher, etc.', + order_8_player='SBa: Player Name; PD: Card #', + order_8_pos='C for Catcher, 1B for first base, P for pitcher, etc.', + order_9_player='SBa: Player Name; PD: Card #', + order_9_pos='C for Catcher, 1B for first base, P for pitcher, etc.', + order_10_player='SBa: Player Name; PD: Card #; only used with a DH', + order_10_pos='P for pitcher; only used with a DH' + ) + @app_commands.checks.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def set_lineup_command( + self, interaction: discord.Interaction, team_abbrev: str, order_1_player: str, order_1_pos: str, + order_2_player: str, order_2_pos: str, order_3_player: str, order_3_pos: str, order_4_player: str, + order_4_pos: str, order_5_player: str, order_5_pos: str, order_6_player: str, order_6_pos: str, + order_7_player: str, order_7_pos: str, order_8_player: str, order_8_pos: str, order_9_player: str, + order_9_pos: str, order_10_player: Optional[str] = None, order_10_pos: Optional[str] = 'P'): + this_game = get_one_game(channel_id=interaction.channel_id, active=True) + if not this_game: + await interaction.response.send_message(f'I dont\'t see an active game in this channel!') + return + + owner_team = await get_game_team(this_game, interaction.user.id) + lineup_team = await get_game_team(this_game, team_abbrev=team_abbrev) + + if not owner_team['id'] in [this_game.away_team_id, this_game.home_team_id]: + await interaction.response.send_message('Bruh. Only GMs of the active teams can set lineups.') + return + if not lineup_team['id'] in [this_game.away_team_id, this_game.home_team_id]: + await interaction.response.send_message(f'I do not see {lineup_team["sname"]} in this game. Please check the team abbrev and ' + f'try again') + return + + existing_lineups = await get_team_lineups(this_game.id, lineup_team['id']) + if not existing_lineups or not len(existing_lineups): + await interaction.response.send_message( + f'It looks like the {lineup_team["sname"]} already have a lineup. Run `/substitution` to make changes.' + ) + return + + await interaction.response.defer() + + all_lineups = [] + all_pos = [] + lineup_data = [ + (order_1_player, order_1_pos), (order_2_player, order_2_pos), (order_3_player, order_3_pos), + (order_4_player, order_4_pos), (order_5_player, order_5_pos), (order_6_player, order_6_pos), + (order_7_player, order_7_pos), (order_8_player, order_8_pos), (order_9_player, order_9_pos), + (order_10_player, order_10_pos) + ] + for index, pair in enumerate(lineup_data): + if not pair[0]: + break + + if pair[1].upper() not in all_pos: + all_pos.append(pair[1].upper()) + else: + raise SyntaxError(f'You have more than one {pair[1].upper()} in this lineup. Please update and set the ' + f'lineup again.') + + if this_game.is_pd: + this_card = pd_get_card_by_id(int(pair[0])) + if this_card['team']['id'] != lineup_team['id']: + raise SyntaxError(f'Easy there, champ. Looks like card ID {pair[0]} belongs to the ' + f'{this_card["team"]["sname"]}. Try again with only cards you own.') + player_id = this_card['player']['id'] + card_id = pair[0] + else: + this_player = await get_one_player(pair[0], season=SBA_SEASON) + if this_player['team']['id'] != lineup_team['id']: + raise SyntaxError(f'Easy there, champ. Looks like {pair[0]} is on ' + f'{this_player["team"]["sname"]}. Try again with only your own players.') + player_id = this_player['id'] + card_id = None + + this_lineup = { + 'game_id': this_game.id, + 'team_id': lineup_team['id'], + 'player_id': player_id, + 'card_id': card_id, + 'position': pair[1].upper(), + 'batting_order': index + 1, + 'after_play': 0 + } + + all_lineups.append(this_lineup) + + logging.info(f'Setting lineup for {owner_team["sname"]} in {"PD" if this_game.is_pd else "SBa"} game') + post_lineups(all_lineups) + + try: + await interaction.edit_original_message(content=None, embed=await self.get_game_state_embed(this_game)) + except IntegrityError as e: + logging.info(f'Unable to pull game_state for game_id {this_game.id} until both lineups are in: {e}') + await interaction.response.send_message(f'Game state will be posted once both lineups are in') + return + + @commands.hybrid_command( + name='substitution', + help='Make a lineup substitution; Player Name for SBa games / Card ID for PD games', + aliases=['sub'] + ) + @app_commands.describe( + new_player='For SBa game: enter the Player Name; for PD game: enter the Card ID (a number)') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def substitution_command( + self, ctx: commands.Context, team_abbrev: str, order_number: int, new_player: str, new_pos: str): + this_game = get_one_game(channel_id=ctx.channel.id, active=True) + if not this_game: + await ctx.send(f'I dont\'t see an active game in this channel!') + return + + owner_team = await get_game_team(this_game, ctx.author.id) + lineup_team = await get_game_team(this_game, team_abbrev=team_abbrev) + + if not owner_team['id'] in [this_game.away_team_id, this_game.home_team_id]: + await ctx.send('Bruh. Only GMs of the active teams can set lineups.') + return + + if not lineup_team['id'] in [this_game.away_team_id, this_game.home_team_id]: + await ctx.send(f'I do not see {lineup_team["sname"]} in this game. Please check the team abbrev and ' + f'try again') + return + + if this_game.is_pd: + this_card = pd_get_card_by_id(int(new_player)) + if this_card["team"]["id"] != lineup_team['id']: + raise SyntaxError(f'Easy there, champ. Looks like card ID {new_player} belongs to the ' + f'{this_card["team"]["sname"]}. Try again with only cards you own.') + player_id = this_card['player']['id'] + card_id = new_player + else: + this_player = await get_one_player(new_player, season=SBA_SEASON) + if this_player['team']['id'] != lineup_team['id']: + raise SyntaxError(f'Easy there, champ. Looks like {new_player} is on ' + f'{this_player["team"]["sname"]}. Try again with only your own players.') + player_id = this_player['id'] + card_id = None + + curr_play = get_current_play(this_game.id) + this_lineup = { + 'game_id': this_game.id, + 'team_id': lineup_team['id'], + 'player_id': player_id, + 'card_id': card_id, + 'position': new_pos, + 'batting_order': order_number, + 'after_play': curr_play.play_num - 1 + } + + make_sub(this_lineup) + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game)) + + @commands.hybrid_command(name='gamestate', help='Post the current game state', aliases=['gs']) + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def game_state_command(self, ctx: commands.Context, include_lineups: bool = True): + this_game = get_one_game(channel_id=ctx.channel.id, active=True) + if not this_game: + await ctx.send(f'I dont\'t see an active game in this channel!') + return + + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=include_lineups)) + + async def checks_log_play(self, ctx: commands.Context) -> (Optional[StratGame], Optional[dict], Optional[StratPlay]): + this_game = get_one_game(channel_id=ctx.channel.id, active=True) + # if not this_game: + # return False, False, False + + owner_team = await get_game_team(this_game, ctx.author.id) + if not owner_team['id'] in [this_game.away_team_id, this_game.home_team_id]: + await ctx.send('Bruh. Only GMs of the active teams can log plays.') + # return this_game, False, False + + this_play = get_current_play(this_game.id) + if this_play.locked: + await ctx.send(f'Looks like this play is already being advanced. Please wait to log the next play.') + if not this_play.pitcher: + await ctx.send(f'Please sub in a pitcher before logging a new play.') + this_play = None + + return this_game, owner_team, this_play + + @commands.hybrid_group(name='log-onbase', help='Log a base hit in this channel\'s game') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def log_onbase(self, ctx: commands.Context): + if ctx.invoked_subcommand is None: + await ctx.send('No play details listed. Type `/log-onbase` to see available commands.') + + @log_onbase.command(name='single-wellhit', help='Batter to first; runners advance two bases', + aliases=['siwh', 'si**', '1b**', '1bwh']) + async def log_single_wh_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + single_wellhit(this_play) + + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @log_onbase.command(name='single-onestar', help='Batter to first; runners advance one base', aliases=['si*', '1b*']) + async def log_single_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + single_onestar(this_play) + + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @log_onbase.command(name='ballpark-single', help='Batter to first; runners advance one base', + aliases=['bpsi', 'bp1b']) + async def log_ballpark_single_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + patch_play(this_play.id, locked=True) + advance_runners(this_play.id, num_bases=1) + patch_play(this_play.id, pa=1, ab=1, hit=1, bp1b=1) + complete_play(this_play.id, batter_to_base=1) + + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @log_onbase.command(name='single-uncapped', help='Batter to first; runners may attempt to advance a second base', + aliases=['si', '1b']) + async def log_single_uncapped_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + patch_play(this_play.id, locked=True, pa=1, ab=1, hit=1) + advance_runners(this_play.id, 1) + this_play = get_current_play(this_game.id) + batter_to_base = 1 + + logging.info(f'this_play: {this_play}') + # Handle runner starting at second + if this_play.on_second: + this_runner = await get_player(this_game, this_play.on_second) + view = Confirm(responders=[ctx.author], timeout=60) + question = await ctx.send(f'Was {this_runner["name"]} sent home?', view=view) + await view.wait() + + if view.value: + await question.delete() + view_two = Confirm(responders=[ctx.author], timeout=60) + question = await ctx.send(content=f'Was {this_runner["name"]} safe at the plate?', view=view_two) + await view_two.wait() + + if view_two.value: + advance_one_runner(this_play.id, from_base=2, num_bases=2) + this_play = patch_play(this_play.id, rbi=this_play.rbi + 1) + else: + this_play = patch_play(this_play.id, on_second_final=99, outs=this_play.outs + 1) + + await question.delete() + else: + await question.delete() + + logging.info(f'this_play: {this_play}') + # Handle runner starting at first + if this_play.on_first and (not this_play.on_second or this_play.on_second_final != 3): + this_runner = await get_player(this_game, this_play.on_first) + view = Confirm(responders=[ctx.author], timeout=60) + question = await ctx.send(f'Was {this_runner["name"]} sent to third?', view=view) + await view.wait() + + if view.value: + await question.delete() + view_two = Confirm(responders=[ctx.author], timeout=60) + question = await ctx.send(content=f'Was {this_runner["name"]} safe at third?', view=view_two) + await view_two.wait() + + if view_two.value: + advance_one_runner(this_play.id, from_base=1, num_bases=2) + this_play = get_current_play(this_game.id) + else: + this_play = patch_play(this_play.id, on_second_final=99, outs=1) + + await question.delete() + else: + await question.delete() + + logging.info(f'this_play: {this_play}') + # Handle batter runner if either runner from first or runner from second advanced + if (this_play.on_first and this_play.on_first_final != 2) or \ + (this_play.on_second and this_play.on_second_final != 3): + batter = await get_player(this_game, this_play.batter) + view = Confirm(responders=[ctx.author], timeout=60) + question = await ctx.send(content=f'Was {batter["name"]} sent to second?', view=view) + await view.wait() + + if view.value: + await question.delete() + view_two = Confirm(responders=[ctx.author], timeout=60) + question = await ctx.send(content=f'Was {batter["name"]} safe at second?', view=view_two) + await view_two.wait() + + if view_two.value: + batter_to_base = 2 + + await question.delete() + else: + await question.delete() + + logging.info(f'this_play: {this_play}') + complete_play(this_play.id, batter_to_base=batter_to_base) + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @log_onbase.command(name='double-twostar', help='Batter to second; runners advance two bases', + aliases=['do**', '2b**']) + async def log_double_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + double_twostar(this_play) + + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @log_onbase.command(name='double-uncapped', help='Batter to second; runners may attempt to advance a third base', + aliases=['do', '2b']) + async def log_double_uncapped_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + patch_play(this_play.id, locked=True, pa=1, ab=1, hit=1, double=1) + + batter_to_base = 2 + if this_play.on_first: + this_runner = await get_player(this_game, this_play.on_first) + view = Confirm(responders=[ctx.author], timeout=60) + question = await ctx.send(f'Was {this_runner["name"]} sent home?', view=view) + await view.wait() + + if view.value: + await question.delete() + view_two = Confirm(responders=[ctx.author], timeout=60) + question = await ctx.send(content=f'Was {this_runner["name"]} safe at the plate?', view=view_two) + await view_two.wait() + + if view_two.value: + advance_runners(this_play.id, num_bases=3) + else: + patch_play(this_play.id, on_first_final=99, outs=1) + + await question.delete() + batter = await get_player(this_game, this_play.batter) + view_three = Confirm(responders=[ctx.author], timeout=60) + question = await ctx.send( + content=f'Did {batter["name"]} attempt the advance to third?', view=view_three + ) + await view_three.wait() + + if view_three.value: + await question.delete() + view_four = Confirm(responders=[ctx.author], timeout=60) + question = await ctx.send(content=f'Was {batter["name"]} safe at third?', view=view_four) + await view_four.wait() + + if view_four.value: + batter_to_base = 3 + await question.delete() + else: + advance_runners(this_play.id, num_bases=2) + await question.delete() + + complete_play(this_play.id, batter_to_base=batter_to_base) + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @log_onbase.command(name='double-threestar', help='Batter to second; runners advance three bases', + aliases=['dowh', 'do***']) + async def log_double_wh_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + double_threestar(this_play) + + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @log_onbase.command(name='triple', help='Batter to third; all runners score', aliases=['tr', '3b']) + async def log_triple_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + triple(this_play) + + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @log_onbase.command(name='homerun', help='Batter scores; all runners score', aliases=['hr', 'dong']) + async def log_homerun_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + patch_play(this_play.id, locked=True) + advance_runners(this_play.id, num_bases=4) + patch_play(this_play.id, pa=1, ab=1, hit=1, homerun=1) + complete_play(this_play.id) + + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @log_onbase.command(name='ballpark-homerun', help='Batter scores; all runners score', aliases=['bp-hr', 'bp-dong']) + async def log_homerun_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + patch_play(this_play.id, locked=True) + advance_runners(this_play.id, num_bases=3) + patch_play(this_play.id, pa=1, ab=1, hit=1, homerun=1, bphr=1) + complete_play(this_play.id) + + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @log_onbase.command(name='walk', help='Batter to first; runners advance if forced', aliases=['bb']) + async def log_walk_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + patch_play(this_play.id, locked=True) + advance_runners(this_play.id, num_bases=1, only_forced=True) + patch_play(this_play.id, pa=1, walk=1) + complete_play(this_play.id, batter_to_base=1) + + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @log_onbase.command(name='intentional-walk', help='Batter to first; runners advance if forced', aliases=['ibb']) + async def log_int_walk_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + patch_play(this_play.id, locked=True) + advance_runners(this_play.id, num_bases=1, only_forced=True) + patch_play(this_play.id, pa=1, ibb=1) + complete_play(this_play.id, batter_to_base=1) + + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @log_onbase.command(name='hit-by-pitch', help='Batter to first; runners advance if forced', aliases=['hbp']) + async def log_hit_by_pitch_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + patch_play(this_play.id, locked=True) + advance_runners(this_play.id, num_bases=1, only_forced=True) + patch_play(this_play.id, pa=1, hbp=1) + complete_play(this_play.id, batter_to_base=1) + + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @commands.hybrid_group(name='log-out', help='Log an out result in this channel\'s game') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def log_out(self, ctx: commands.Context): + if ctx.invoked_subcommand is None: + await ctx.send('No play details listed. Type `/log-out` to see available commands.') + + @log_out.command(name='popout', help='Batter out; runners hold', aliases=['po']) + async def log_popout_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + patch_play(this_play.id, locked=True, pa=1, ab=1, outs=1) + advance_runners(this_play.id, num_bases=0) + complete_play(this_play.id) + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @log_out.command(name='strikeout', help='Batter out; runners hold', aliases=['so', 'k']) + async def log_strikeout_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + patch_play(this_play.id, locked=True, pa=1, ab=1, outs=1, so=1) + advance_runners(this_play.id, num_bases=0) + complete_play(this_play.id) + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @log_out.command(name='lineout', help='Batter out; runners hold', aliases=['lo']) + async def log_lineout_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + patch_play(this_play.id, locked=True, pa=1, ab=1, outs=1) + advance_runners(this_play.id, num_bases=0) + complete_play(this_play.id) + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @log_out.command(name='sac-bunt', help='Batter out; runners advance one base', aliases=['sacb', 'bunt']) + async def log_sac_bunt_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + patch_play(this_play.id, locked=True) + advance_runners(this_play.id, num_bases=1) + patch_play(this_play.id, pa=1, sac=1, outs=1) + complete_play(this_play.id) + + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @log_out.command(name='caught-stealing', help='One base runner is caught stealing', aliases=['cs']) + @app_commands.describe( + attempted_base='The base number the runner attempted to steal; 2 for 2nd, 3 for 3rd, 3 for home') + async def log_caught_stealing_command(self, ctx: commands.Context, attempted_base: int): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + patch_play(this_play.id, locked=True) + catcher = get_one_lineup( + this_game.id, team_id=this_play.pitcher.team_id, position='C' + ) + + if attempted_base == 4 and this_play.on_third: + patch_play( + this_play.id, cs=1, on_third_final=99, runner_id=this_play.on_third.id, + catcher_id=catcher.id, outs=1 + ) + elif attempted_base == 3 and this_play.on_second: + if not this_play.on_third: + patch_play( + this_play.id, cs=1, on_second_final=99, runner_id=this_play.on_second.id, + catcher_id=catcher.id, outs=1 + ) + else: + this_runner = await get_player(this_game, this_play.on_second) + await ctx.send(f'Ope. Looks like {this_runner["name"]} is blocked by the runner at third.') + return + elif attempted_base == 2 and this_play.on_first: + if not this_play.on_second: + patch_play( + this_play.id, cs=1, on_first_final=99, runner_id=this_play.on_first.id, + catcher_id=catcher.id, outs=1 + ) + else: + this_runner = await get_player(this_game, this_play.on_first) + await ctx.send(f'Ope. Looks like {this_runner["name"]} is blocked by the runner at second.') + return + else: + await ctx.send(f'Uh oh - I don\'t see a runner there to steal the bag.') + return + + complete_play(this_play.id) + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @log_out.command(name='flyball-a', help='Batter out; all runners advance', aliases=['flya']) + async def log_flyballa_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + patch_play(this_play.id, locked=True, pa=1, ab=1, outs=1) + if this_play.starting_outs < 2: + advance_runners(this_play.id, 1) + if this_play.on_third: + patch_play(this_play.id, ab=0) + + complete_play(this_play.id) + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @log_out.command(name='flyball-b', help='Batter out; runner on third scores', aliases=['flyb']) + async def log_flyballb_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + patch_play(this_play.id, locked=True, pa=1, ab=1, outs=1) + advance_runners(this_play.id, num_bases=0) + if this_play.starting_outs < 2 and this_play.on_third: + patch_play(this_play.id, ab=0, rbi=1) + advance_one_runner(this_play.id, from_base=3, num_bases=1) + + if this_play.starting_outs < 2 and this_play.on_second: + view = Confirm(responders=[ctx.author], timeout=60) + question = await ctx.send('Did the runner on second advance?', view=view) + await view.wait() + + if view.value: + advance_one_runner(this_play.id, from_base=2, num_bases=1) + await question.edit(view=None) + else: + await question.delete() + + complete_play(this_play.id) + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @log_out.command(name='flyball-bq', help='Batter out; runner on third may attempt to score', aliases=['flyb?']) + async def log_flyballbq_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + patch_play(this_play.id, locked=True, pa=1, ab=1, outs=1) + advance_runners(this_play.id, num_bases=0) + if this_play.starting_outs < 2 and this_play.on_third: + view = Confirm(responders=[ctx.author], timeout=60) + question = await ctx.send('Did the runner on third advance?', view=view) + await view.wait() + + if view.value: + advance_one_runner(this_play.id, from_base=3, num_bases=1) + patch_play(this_play.id, ab=0, rbi=1) + await question.edit(view=None) + else: + await question.delete() + + complete_play(this_play.id) + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @log_out.command(name='flyball-c', help='Batter out; no runners advance', aliases=['flyc']) + async def log_flyballc_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + patch_play(this_play.id, locked=True, pa=1, ab=1, outs=1) + advance_runners(this_play.id, num_bases=0) + complete_play(this_play.id) + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @commands.hybrid_group(name='log-play', help='Log a result in this channel\'s game') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def log_play(self, ctx: commands.Context): + if ctx.invoked_subcommand is None: + await ctx.send('No play details listed. Type `/log` to see available commands.') + + @log_play.command(name='undo-play', help='Remove the most recent play from the log', aliases=['undo', 'rollback']) + async def log_undo_play_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + undo_play(this_play.id) + undo_play(get_current_play(this_game.id).id) + complete_play(get_current_play(this_game.id).id, batter_to_base=get_latest_play(this_game.id).batter_final) + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @log_play.command(name='stolen-base', help='One base runner steals a base', aliases=['sb']) + @app_commands.describe(stolen_base='The base number stolen by the runner; 2 for 2nd, 3 for 3rd, 4 for home') + async def log_stolen_base_command(self, ctx: commands.Context, stolen_base: int): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + patch_play(this_play.id, locked=True) + catcher = get_one_lineup( + this_game.id, team_id=this_play.pitcher.team_id, position='C' + ) + + if stolen_base == 4 and this_play.on_third: + patch_play( + this_play.id, sb=1, on_third_final=4, runner_id=this_play.on_third.id, catcher_id=catcher.id + ) + elif stolen_base == 3 and this_play.on_second: + if not this_play.on_third: + patch_play( + this_play.id, sb=1, on_second_final=3, runner_id=this_play.on_second.id, catcher_id=catcher.id + ) + else: + this_runner = await get_player(this_game, this_play.on_second) + await ctx.send(f'Ope. Looks like {this_runner["name"]} is blocked by the runner at third.') + patch_play(this_play.id, locked=False) + return + elif stolen_base == 2 and this_play.on_first: + if not this_play.on_second: + patch_play( + this_play.id, sb=1, on_first_final=2, runner_id=this_play.on_first.id, catcher_id=catcher.id + ) + else: + this_runner = await get_player(this_game, this_play.on_first) + await ctx.send(f'Ope. Looks like {this_runner["name"]} is blocked by the runner at second.') + patch_play(this_play.id, locked=False) + return + else: + await ctx.send(f'Uh oh - I don\'t see a runner there to steal the bag.') + patch_play(this_play.id, locked=False) + return + + complete_play(this_play.id) + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @log_play.command(name='wild-pitch', help='All runners advance one base', aliases=['wp']) + async def log_wild_pitch_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + advance_runners(this_play.id, 1) + patch_play(this_play.id, rbi=0, wp=1) + complete_play(this_play.id) + + @log_play.command(name='passed-ball', help='All runners advance one base', aliases=['pb']) + async def log_passed_ball_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + advance_runners(this_play.id, 1) + patch_play(this_play.id, rbi=0, pb=1) + complete_play(this_play.id) + + @log_play.command(name='balk', help='All runners advance one base', aliases=['bk']) + async def log_balk_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + advance_runners(this_play.id, 1) + patch_play(this_play.id, rbi=0, balk=1) + complete_play(this_play.id) + + @log_play.command(name='pickoff', help='One baserunner is out on the basepaths', aliases=['pick']) + async def log_pickoff_command(self, ctx: commands.Context, from_base: int): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + patch_play(this_play.id, locked=True) + + if from_base == 3 and this_play.on_third: + patch_play(this_play.id, on_third_final=99, runner_id=this_play.on_third.id, outs=1) + elif from_base == 2 and this_play.on_second: + patch_play(this_play.id, on_second_final=99, runner_id=this_play.on_second.id, outs=1) + elif from_base == 1 and this_play.on_first: + patch_play(this_play.id, on_first_final=99, runner_id=this_play.on_first.id, outs=1) + else: + await ctx.send(f'Uh oh - I don\'t see a runner there to be picked off.') + patch_play(this_play.id, locked=False) + return + + complete_play(this_play.id) + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @commands.hybrid_group(name='show-card', help='Display an active player\'s card') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def show_player(self, ctx: commands.Context): + if ctx.invoked_subcommand is None: + await ctx.send('No player details listed. Type `/show-player` to see available commands.') + + @show_player.command(name='defense', help='Display a defender\'s player card', aliases=['pick']) + async def show_defense_command( + self, ctx: commands.Context, position: Literal[ + 'Catcher', 'First Base', 'Second Base', 'Third Base', 'Shortstop', 'Left Field', 'Center Field', + 'Right Field']): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + defender = await get_player( + game=this_game, + lineup_member=get_one_lineup( + this_game.id, team_id=this_play.pitcher.team_id, position=get_pos_abbrev(position) + ) + ) + + embed = get_team_embed(f'{defender["team"]["sname"]} {position}', defender['team']) + embed.description = f'{defender["name"]}' + embed.set_image(url=defender['image']) + if this_game.is_pd: + embed.set_footer(text=f'PD Season {PD_SEASON}', icon_url=LOGO) + + await ctx.send(content=None, embed=embed) + + @log_out.command(name='groundball-a', help='Potential double play ground ball', aliases=['gba']) + async def log_groundballa_command(self, ctx: commands.Context): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + batter_to_base = None + patch_play(this_play.id, locked=True) + if this_play.starting_outs == 2 or this_play.on_base_code == 0: + patch_play(this_play.id, pa=1, ab=1, outs=1) + else: + if this_play.on_base_code == 1: + runner = await get_player(this_game, this_play.on_first) + view = Confirm(responders=[ctx.author], timeout=60) + question = await ctx.send(f'Is {runner["name"]} out at second on the double play?', view=view) + await view.wait() + + if view.value: + await question.delete() + patch_play(this_play.id, on_first_final=False, pa=1, ab=1, outs=2) + else: + await question.delete() + await ctx.send(f'Okay so it wasn\'t a gb A then? Go ahead and log a new play.') + patch_play(this_play.id, locked=False) + return + + elif this_play.on_base_code == 7: + runner = await get_player(this_game, this_play.on_third) + view = Confirm(responders=[ctx.author], timeout=60) + question = await ctx.send(f'Is {runner["name"]} out on the home-to-first double play?', view=view) + await view.wait() + + if view.value: + await question.delete() + advance_runners(this_play.id, 1) + patch_play(this_play.id, on_third_final=False, pa=1, ab=1, outs=2, rbi=0) + else: + await question.delete() + runner = await get_player(this_game, this_play.on_second) + view = Confirm(responders=[ctx.author], timeout=60) + question = await ctx.send(f'Is {runner["name"]} out at second on the double play?', view=view) + await view.wait() + + if view.value(): + await question.delete() + advance_runners(this_play.id, 1) + patch_play(this_play.id, on_first_final=False, pa=1, ab=1, outs=2, rbi=0) + else: + await question.delete() + await ctx.send(f'Okay so it wasn\'t a gb A then? Go ahead and log a new play.') + patch_play(this_play.id, locked=False) + return + + else: + num_outs = 1 + bases = ['third', 'second', 'first'] + + for count, x in enumerate([this_play.on_third, this_play.on_second, this_play.on_first]): + if x: + runner = await get_player(this_game, x) + view = Confirm(responders=[ctx.author], timeout=60) + question = await ctx.send( + f'Did {runner["name"]} advance from {bases[count]} on the play?', view=view + ) + await view.wait() + + num_bases = 0 + if view.value: + await question.delete() + advance_one_runner(this_play.id, from_base=3 - count, num_bases=1) + else: + await question.delete() + view = Confirm(responders=[ctx.author], timeout=60) + question = await ctx.send(f'Was {runner["name"]} doubled off?', view=view) + await view.wait() + + if view.value: + await question.delete() + if count == 0: + patch_play(this_play.id, on_third_final=False) + elif count == 1: + patch_play(this_play.id, on_second_final=False) + else: + patch_play(this_play.id, on_first_final=False) + num_outs += 1 + else: + await question.delete() + if count == 0: + patch_play(this_play.id, on_third_final=3) + elif count == 1: + patch_play(this_play.id, on_second_final=2) + else: + patch_play(this_play.id, on_first_final=1) + + if this_play.on_third: + batter = await get_player(this_game, this_play.batter) + view = Confirm(responders=[ctx.author], timeout=60) + question = await ctx.send( + f'Is {batter["name"]} out at first?', view=view + ) + await view.wait() + + if view.value: + await question.delete() + else: + await question.delete() + num_outs -= 1 + batter_to_base = 1 + + patch_play(this_play.id, pa=1, ab=1, outs=num_outs) + + complete_play(this_play.id, batter_to_base=batter_to_base) + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + @log_play.command(name='xcheck', help='Defender makes an x-check') + async def log_xcheck_command(self, ctx: commands.Context, position: Literal[ + 'Catcher', 'First Base', 'Second Base', 'Third Base', 'Shortstop', 'Left Field', 'Center Field', + 'Right Field'], hit_allowed: Literal['out', 'single*', 'single**', 'double**', 'double***', 'triple'], + error_allowed: Literal['out', '1 base', '2 bases', '3 bases']): + this_game, owner_team, this_play = await self.checks_log_play(ctx) + if False in (this_game, owner_team, this_play): + return + + pos = get_pos_abbrev(position) + defender = get_one_lineup( + this_game.id, team_id=this_play.pitcher.team_id, position=pos + ) + logging.info(f'defender: {defender}') + patch_play(this_play.id, defender_id=defender.id, error=1 if error_allowed != 'out' else 0, check_pos=pos) + + # Not hit and no error + if hit_allowed == 'out' and error_allowed == 'out': + await ctx.send(f'Just logged the x-check! Please log the resulting play to continue (e.g. \'flyball-b\' or ' + f'\'groundball-a\')') + return + + # Hit and error + if hit_allowed != 'out' and error_allowed != 'out': + batter_to_base = 1 + if hit_allowed == 'triple': + triple(this_play, comp_play=False) + elif 'double' in hit_allowed: + double_threestar(this_play, comp_play=False) + if error_allowed == '1 base': + batter_to_base = 3 + elif error_allowed == '3 bases': + batter_to_base = 4 + # 2 base error is the only one handled differently between doubles + elif hit_allowed == 'double***': + batter_to_base = 4 + else: + batter_to_base = 3 + # Both singles are handled the same + else: + single_wellhit(this_play, comp_play=False) + if error_allowed == '1 base': + batter_to_base = 2 + else: + batter_to_base = 3 + + complete_play(this_play.id, batter_to_base=batter_to_base) + + # Either hit or error + num_bases = None + if error_allowed == 'out': + if hit_allowed == 'single*': + single_onestar(this_play) + elif hit_allowed == 'single**': + single_wellhit(this_play) + elif hit_allowed == 'double**': + double_twostar(this_play) + elif hit_allowed == 'double***': + double_threestar(this_play) + elif hit_allowed == 'triple': + triple(this_play) + else: + if error_allowed == '1 base': + num_bases = 1 + elif error_allowed == '2 bases': + num_bases = 2 + elif error_allowed == '3 bases': + num_bases = 3 + + advance_runners(this_play.id, num_bases=num_bases, is_error=True) + complete_play(this_play.id, batter_to_base=num_bases) + + await ctx.send(content=None, embed=await self.get_game_state_embed(this_game, full_length=False)) + + # TODO: ground balls + + +async def setup(bot): + await bot.add_cog(Gameplay(bot)) + diff --git a/cogs/owner.py b/cogs/owner.py new file mode 100644 index 0000000..7754c71 --- /dev/null +++ b/cogs/owner.py @@ -0,0 +1,123 @@ +import logging +import os + +import discord +from discord import Object +from discord.ext import commands +from discord.ext.commands import Greedy, Context +from typing import Optional, Literal + + +class Owner(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.command(hidden=True) + @commands.is_owner() + async def load(self, ctx, *, cog: str): + try: + await self.bot.load_extension(f'cogs.{cog}') + logging.warning(f'Loaded {cog}') + except Exception as e: + await ctx.send(f'**ERROR:** {type(e).__name__} - {e}') + else: + await ctx.send('**SUCCESS**') + + @commands.command(hidden=True) + @commands.is_owner() + async def unload(self, ctx, *, cog: str): + try: + await self.bot.unload_extension(f'cogs.{cog}') + logging.warning(f'Unloaded {cog}') + except Exception as e: + await ctx.send(f'**ERROR:** {type(e).__name__} - {e}') + else: + await ctx.send('**SUCCESS**') + + @commands.command(hidden=True) + @commands.is_owner() + async def reload(self, ctx, *, cog: str): + try: + await self.bot.unload_extension(f'cogs.{cog}') + logging.warning(f'Unloaded {cog}') + await self.bot.load_extension(f'cogs.{cog}') + logging.warning(f'Reloaded {cog}') + except Exception as e: + await ctx.send(f'**ERROR:** {type(e).__name__} - {e}') + else: + await ctx.send('**SUCCESS**') + + @commands.command(hidden=True) + @commands.is_owner() + async def fullreset(self, ctx): + cogs = ['players', 'transactions', 'admins', 'dice', 'fun', 'gameday'] + + for x in cogs: + try: + await self.bot.unload_extension(f'cogs.{x}') + logging.warning(f'Unloaded {x}') + except Exception as e: + await ctx.send(f'Failed to unload **{x}**') + + for x in cogs: + try: + await self.bot.load_extension(f'cogs.{x}') + logging.warning(f'Loaded {x}') + except Exception as e: + await ctx.send(f'Failed to load **{x}**') + + await ctx.send('**SUCCESS**') + + # @commands.command(name='sync', hidden=True) + # @commands.is_owner() + # async def sync_command(self, ctx, sync_type: str = 'local'): + # sync = None + # await ctx.send('I will try to sync slash commands...') + # try: + # if sync_type == 'global': + # sync = await self.bot.tree.sync() + # else: + # sync = await self.bot.tree.sync(guild=discord.Object(os.environ.get('GUILD_ID'))) + # logging.warning(f'sync: {sync}') + # except Exception as e: + # logging.error(f'failed to sync: {e}') + # + # await ctx.send(f'Just ran the sync. Here is the output:\n{sync}') + + @commands.command() + @commands.is_owner() + async def sync(self, ctx: Context, guilds: Greedy[Object], spec: Optional[Literal['~', "*"]] = None) -> None: + """ + !sync -> global sync + !sync ~ -> sync current guild + !sync * -> copies all global app commands to current guild and syncs + !sync id_1 id_2 -> syncs guilds with id 1 and 2 + """ + if not guilds: + if spec == "~": + fmt = await ctx.bot.tree.sync(guild=ctx.guild) + elif spec == "*": + ctx.bot.tree.copy_global_to(guild=ctx.guild) + fmt = await ctx.bot.tree.sync(guild=ctx.guild) + else: + fmt = await ctx.bot.tree.sync() + + await ctx.send( + f"Synced {len(fmt)} commands {'globally' if spec is None else 'to the current guild.'}" + ) + return + + fmt = 0 + for guild in guilds: + try: + await ctx.bot.tree.sync(guild=guild) + except discord.HTTPException: + pass + else: + fmt += 1 + + await ctx.send(f"Synced the tree to {fmt}/{len(guilds)} guilds.") + + +async def setup(bot): + await bot.add_cog(Owner(bot)) diff --git a/cogs/players.py b/cogs/players.py new file mode 100644 index 0000000..d23df3a --- /dev/null +++ b/cogs/players.py @@ -0,0 +1,2967 @@ +import math +import pydantic +import re + +from discord import app_commands +from discord.ext import tasks + +from db_calls_gameplay import get_one_game +from helpers import * +from db_calls import * +from typing import Literal + + +class BatStat(pydantic.BaseModel): + player_id: int + team_id: int + pos: str + pa: int = 0 + ab: int = 0 + run: int = 0 + hit: int = 0 + rbi: int = 0 + double: int = 0 + triple: int = 0 + hr: int = 0 + bb: int = 0 + so: int = 0 + hbp: int = 0 + sac: int = 0 + ibb: int = 0 + gidp: int = 0 + sb: int = 0 + cs: int = 0 + bphr: int = 0 + bpfo: int = 0 + bp1b: int = 0 + bplo: int = 0 + xba: int = 0 + xbt: int = 0 + xch: int = 0 + xhit: int = 0 + error: int = 0 + pb: int = 0 + sbc: int = 0 + csc: int = 0 + roba: int = 0 + robs: int = 0 + raa: int = 0 + rto: int = 0 + week: int + game: int + season: int + + +class PitStat(pydantic.BaseModel): + player_id: int + team_id: int + ip: float = 0.0 + hit: int = 0 + run: int = 0 + erun: int = 0 + so: int = 0 + bb: int = 0 + hbp: int = 0 + wp: int = 0 + balk: int = 0 + hr: int = 0 + gs: int = 0 + win: int = 0 + loss: int = 0 + hold: int = 0 + sv: int = 0 + bsv: int = 0 + ir: int = 0 + irs: int = 0 + week: int + game: int + season: int + + +class Players(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.player_list = {} + self.scorecards = {} + + self.build_master_player_list.start() + + async def cog_command_error(self, ctx, error): + await ctx.send(f'{error}\n\nRun !help to see the command requirements') + + async def slash_error(self, ctx, error): + await ctx.send(f'{error}') + + @tasks.loop(count=1) + async def build_master_player_list(self): + current = await get_current() + all_players = await get_players(current['season']) + self.player_list = {all_players[player]['name'].lower(): all_players[player]['id'] for player in all_players} + logging.debug(f'player list: {self.player_list}') + logging.warning(f'player list count: {len(self.player_list)}') + + @staticmethod + async def update_injuries(ctx): + current = await get_current() + injury_log = discord.utils.get(ctx.guild.text_channels, name='injury-log') + + # Build new messages + # inj_by_team = {'Black Bears': [Player Objs], 'Questants': [Player Objs]} + # inj_by_week = {'Week 1': [Player Objs], 'Week 2': [Player Objs]} + inj_team = {} + inj_week = {} + all_injuries = await get_players(current['season'], injured=True) + + for x in all_injuries: + player = all_injuries[x] + this_team = await get_major_team(player['team']) + + if this_team['sname'] not in inj_team.keys(): + inj_team[this_team['sname']] = [player] + else: + inj_team[this_team['sname']].append(player) + + if f'Week {player["il_return"][1:3]}' not in inj_week.keys(): + inj_week[f'Week {player["il_return"][1:3]}'] = [player] + else: + inj_week[f'Week {player["il_return"][1:3]}'].append(player) + + inj_by_team = dict(sorted(inj_team.items())) + inj_by_week = dict(sorted(inj_week.items())) + + team_embed = discord.Embed(title='Current Injuries by Team') + team_embed.description = 'Player Name (Return Date)' + team_embed.set_thumbnail(url=LOGO) + for team in inj_by_team: + team_string = '' + for player in inj_by_team[team]: + team_string += f'{player["name"]} ({player["il_return"]})\n' + team_embed.add_field(name=team, value=team_string) + + week_embed = discord.Embed(title='Current Injuries by Return Week') + week_embed.description = 'Player Name (Return Date)' + week_embed.set_thumbnail(url=LOGO) + for week in inj_by_week: + week_string = '' + for player in inj_by_week[week]: + week_string += f'{player["name"]} ({player["il_return"]})\n' + week_embed.add_field(name=week, value=week_string) + + # Clear old messages + async for message in injury_log.history(limit=25): + await message.delete() + + # Send new messages + await injury_log.send(content=None, embed=team_embed) + await injury_log.send(content=None, embed=week_embed) + + @staticmethod + def team_stan_line(stan, s_type: str = 'div'): + if stan["wins"] + stan["losses"] == 0: + winpct = f'{0:.3f}' + else: + winpct = f'{stan["wins"] / (stan["wins"] + stan["losses"]):.3f}' + team_string = f'{stan["team"]["abbrev"]: <4} {stan["wins"]: >2}-{stan["losses"]: <2} {winpct} ' + + if s_type == 'div': + if stan["div_gb"] < 0: + gb = stan["div_gb"] if stan["div_gb"] >= 0.0 else f'+{stan["div_gb"] * -1}' + else: + gb = stan["div_gb"] + team_string += f'{gb: >5}' + + if stan["div_e_num"] is None: + team_string += f' -- ' + else: + e_num = stan["div_e_num"] if stan["div_e_num"] > 0 else 'E' + team_string += f' {e_num: >2} ' + else: + if stan["wc_gb"] is None: + # This is a division leader - return nothing and continue + return '' + else: + gb = stan["wc_gb"] if stan["wc_gb"] >= 0.0 else f'+{stan["wc_gb"] * -1}' + if stan["wc_e_num"]: + e_num = stan["wc_e_num"] + elif stan["wc_e_num"] == 0: + e_num = 'E' + else: + e_num = '--' + team_string += f'{gb: >5} {e_num: >2} ' + + team_string += f'{stan["run_diff"]: >4}\n' + + return team_string + + @staticmethod + async def game_progress(current): + this_week = await get_schedule( + current['season'], + week_start=current['week'], + week_end=current['week'] + ) + results_this_week = await get_results( + current['season'], + week=current['week'] + ) + + game_count = 0 + for x in this_week: + game_count += this_week[x]['gamecount'] + + return {'games_played': len(results_this_week), 'game_count': game_count} + + @commands.Cog.listener(name='on_message') + async def on_message_listener(self, message): + # if 'bad man' in message.content: + # await message.channel.send( + # 'https://cdn.discordapp.com/attachments/619600872782954539/682411826335711268/image0.jpg' + # ) + if message.author.bot: + return + + tm = message.content.lower() + + if tm in ['shut up, jack', 'shut up jack']: + await message.channel.send('Shut up, Jack') + return + # elif message.content[-3:] == '...': + # await message.channel.send('https://media.tenor.com/images/423f15eef7688d3010c4d83a16902574/tenor.gif') + # return + elif 'DELIBERAT' in message.content: + await message.channel.send('https://tenor.com/view/fbi-swat-jelleton-gif-14190942') + elif 'domo sux' in tm or 'domo sucks' in tm or 'fuck domo' in tm or 'fuck you domo' in tm: + await message.add_reaction('🖕') + elif 'joe momma' in tm or 'joe mama' in tm: + await message.channel.send('https://tenor.com/view/shaq-big-dunk-basketball-chris-dudley-shaquille-' + 'o-neal-gif-13864249') + elif 'i\'m dad' in tm or 'im dad' in tm: + await message.channel.send( + 'https://tenor.com/view/dad-jokes-aht-aht-dad-jokes-aht-aht-ha-ha-ha-knee-slapper-gif-26152690' + ) + # elif 'fifa' in tm or 'soccer' in tm or 'world cup' in tm or 'the wc' in tm or 'this wc' in tm or 'futbol' in tm: + # randint = random.randint(1, 5) + # if randint == 5: + # await message.channel.send(f'||{message.content}\n{message.author.name}, apparently||\n\n' + # f'Ugh, I can\'t with this soccer talk.') + # await message.delete() + # else: + # await message.channel.send(random_soccer()) + + if message.channel.name in ['trade-block', 'jacks-trading-post']: + count = 0 + async for x in message.channel.history(): + if x.author.id == message.author.id and x.id != message.id: + count += 1 + await x.delete() + if count > 1: + await message.author.send(f'I have deleted your previous {count} trade-block messages to ' + f'keep it clean.') + elif count == 1: + await message.author.send('I deleted your last trade-block message to help keep it clean.') + + # if len(message.content) == 1 and not message.content.isnumeric() and message.content.lower() != 'k': + # logging.info( + # f'Found spam from **{message.author.nick}** in **{message.channel.name}**: {message.content}' + # ) + # if 'shitpost' not in message.channel.name and 'sbb' not in message.channel.name: + # await send_to_channel( + # self.bot, + # 'commissioners-office', + # f'Just deleted this message from **{message.author.nick}** in **{message.channel.name}**:' + # f'\n\n{message.content}' + # ) + # await message.delete() + # await message.author.send( + # f'Hi there. I just deleted your \'{message.content}\' message from #{message.channel.name}. I am ' + # f'told to delete 1-character messages due to spam.' + # ) + + @staticmethod + def get_standings_embed(current, progress, al_standings, nl_standings): + embed = discord.Embed(title=f'Season {current["season"]} | Week {current["week"]} | ' + f'{progress["games_played"]}/{progress["game_count"]} games played', + color=0xB70000) + embed.add_field(name=f'**Full Standings**', value=SBA_STANDINGS_URL, inline=False) + embed.add_field(name=f'**American League**', value=al_standings, inline=False) + embed.add_field(name=f'**National League**', value=nl_standings, inline=False) + + return embed + + @commands.command(name='team', aliases=['roster', 'myboys', 'mybois'], help='Get team overview') + async def team_command(self, ctx, *abbrev): + current = await get_current() + + # Get Team + if abbrev: + team = await get_one_team(abbrev[0]) + else: + team = await get_team_by_owner(current['season'], ctx.author.id) + + # Create team embed + embed = get_team_embed(f'{team["lname"]} Overview', team) + + # Get standings + if team['abbrev'][-2:].lower() != 'il': + try: + team_standings = await get_standings(current['season'], team_abbrev=team['abbrev']) + if team_standings: + overview_string = f'Record: {team_standings["wins"]}-{team_standings["losses"]} ' \ + f'({team_standings["run_diff"]} RD)\n' \ + f'Pythag Record: {team_standings["pythag_wins"]}-{team_standings["pythag_losses"]}\n' + division_string = '' + if team_standings['div_gb']: + division_string += f'{team_standings["div_gb"]} GB in ' \ + f'{team_standings["team"]["division"]["league_abbrev"]} ' \ + f'{team_standings["team"]["division"]["division_name"]} / ' \ + f'{team_standings["wc_gb"]} GB in ' \ + f'{team_standings["team"]["division"]["league_abbrev"]} WC\n' + else: + division_string += f'1st in {team_standings["team"]["division"]["league_abbrev"]} ' \ + f'{team_standings["team"]["division"]["division_name"]}\n' + + overview_string += division_string + overview_string += f'Last 8: ({team_standings["last8_wins"]}-{team_standings["last8_losses"]}) / ' \ + f'Streak: {team_standings["streak_wl"].upper()}{team_standings["streak_num"]}' + + embed.add_field(name=f'{team["sname"]} Overview', value=overview_string, inline=False) + except ValueError as e: + logging.info(f'Could not pull standings for season {team["season"]} {team["abbrev"]}') + + # Get player info + il_players = None + if team['abbrev'][-2:].lower() != 'il': + il_players = await get_players(current['season'], team_abbrev=f'{team["abbrev"]}IL', sort='wara-desc') + players = await get_players(current['season'], team_abbrev=team['abbrev'], sort='wara-desc') + il_wara = 0 + active_wara = 0 + + if il_players: + for x in il_players: + il_wara += il_players[x]['wara'] + + if players: + count = 0 + top_player_string = '' + + for x in players: + if count < 5: + top_player_string += f'{players[x]["pos_1"]} {players[x]["name"]} ({players[x]["wara"]:.2f})\n' + active_wara += players[x]['wara'] + count += 1 + + embed.add_field(name='Core Players', value=top_player_string) + embed.add_field(name='Total sWAR', value=f'{active_wara:.2f}') + if il_wara > 0: + embed.add_field(name='Injured sWAR', value=f'{il_wara:.2f}') + + # Get near schedule + team_schedule = await get_schedule( + current['season'], + team_abbrev1=team["abbrev"], + week_start=current['week'], + week_end=current['week']+2 if current['week']+2 > 0 else 2, + ) + if team_schedule: + this_week_string = '' + upcoming_string = '' + + full_sched = dict(sorted(team_schedule.items(), key=lambda item: item[1]["id"])) + + for matchup in full_sched: + if full_sched[matchup]['hometeam'] == team: + opp_record = await get_standings( + season=current['season'], + team_abbrev=full_sched[matchup]['awayteam']['abbrev'] + ) + else: + opp_record = await get_standings( + season=current['season'], + team_abbrev=full_sched[matchup]['hometeam']['abbrev'] + ) + if full_sched[matchup]['week'] == current['week']: + if full_sched[matchup]['hometeam'] == team: + this_week_string += f'Week {current["week"]}: vs ' \ + f'{full_sched[matchup]["awayteam"]["lname"]} ' \ + f'({opp_record["wins"]}-{opp_record["losses"]})\n' + else: + this_week_string += f'Week {current["week"]}: @ {full_sched[matchup]["hometeam"]["lname"]} ' \ + f'({opp_record["wins"]}-{opp_record["losses"]})\n' + else: + if full_sched[matchup]['hometeam'] == team: + upcoming_string += f'Week {full_sched[matchup]["week"]}: vs ' \ + f'{full_sched[matchup]["awayteam"]["lname"]} ' \ + f'({opp_record["wins"]}-{opp_record["losses"]})\n' + else: + upcoming_string += f'Week {full_sched[matchup]["week"]}: @ ' \ + f'{full_sched[matchup]["hometeam"]["lname"]} ' \ + f'({opp_record["wins"]}-{opp_record["losses"]})\n' + + if len(this_week_string) > 0: + embed.add_field(name='This Week', value=this_week_string, inline=False) + if len(upcoming_string) > 0: + embed.add_field(name='Upcoming Schedule', value=upcoming_string, inline=False) + + # Add roster link + embed.add_field( + name=f'{team["abbrev"]} Roster Page', + value=f'https://sombaseball.ddns.net/teams?abbrev={team["abbrev"]}', + inline=False + ) + + await ctx.send(content=None, embed=embed) + + @commands.command(name='player', aliases=['card'], help='Get player overview') + async def player_command(self, ctx, *, name): + current = await get_current() + + name_reg = re.compile(r'(s\d+)* *(.*)', re.IGNORECASE) + player_search = name_reg.search(name) + logging.info(f'player_search: {player_search}') + logging.info(f'player_search.group(): {player_search.group()} / group(1): {player_search.group(1)} ' + f'/ group(2): {player_search.group(2)}') + + # No season is included + if not player_search.group(1): + player = await get_one_player( + await fuzzy_player_search(ctx, ctx.channel, self.bot, name, self.player_list.keys()) + ) + # Season is included + else: + season = int(player_search.group(1)[1:]) + + try: + player = await get_one_player(player_search.group(2), season=season) + except Exception as e: + logging.error(e) + async with ctx.typing(): + all_players = await get_players(season) + all_player_names = {all_players[player]['name'].lower(): all_players[player]['id'] for player in all_players} + player = await get_one_player( + await fuzzy_player_search(ctx, ctx.channel, self.bot, player_search.group(2).strip(), all_player_names), + season=season + ) + + embed = await get_player_embed(player, current, ctx) + + await ctx.send(content=None, embed=embed) + if player['image2']: + embed = get_team_embed(f'{player["name"]}', player["team"], thumbnail=False) + embed.set_image(url=player['image2']) + await ctx.send(content=None, embed=embed) + + @commands.command(name='career', help='Get player\'s career stats') + async def career_command(self, ctx, *, name): + # Get BattingCareer + b = await get_battingcareer(name) + # Get PitchingCareer + p = await get_pitchingcareer(name) + if not b and not p: + await ctx.send('Who? Please splel berter') + return + + player = { + 'name': b['name'] if b else p['name'], + } + + # Build custom embed + embed = get_team_embed(f'{player["name"]} Career Stats') + embed.color = int('0xa6ce39', 16) + player_photo = await get_player_headshot(b['name']) + if player_photo: + embed.set_thumbnail(url=player_photo) + player_pages = f'[SBa]({get_player_url(player)}) / ' \ + f'[BBRef]({get_player_url(player, "bbref")})' + embed.add_field(name='Player Page', value=player_pages) + + batting_string = None + pitching_string = None + + if b: + if b['ab'] > 0: + singles = b['hit'] - b['hr'] - b['triple'] - b['double'] + avg = b['hit'] / b['ab'] + obp = (b['hit'] + b['bb'] + b['ibb'] + b['hbp']) / b['pa'] + slg = ((b['hr'] * 4) + (b['triple'] * 3) + (b['double'] * 2) + singles) / b['ab'] + ops = obp + slg + woba = ((b['bb'] * .69) + (b['hbp'] * .72) + (singles * .89) + (b['double'] * 1.27) + + (b['triple'] * 1.62) + (b['hr'] * 2.1)) / (b['pa'] - b['hbp'] - b['sac']) + ab = f'{b["ab"]:.0f}' + run = f'{b["run"]:.0f}' + hit = f'{b["hit"]:.0f}' + double = f'{b["double"]:.0f}' + triple = f'{b["triple"]:.0f}' + hr = f'{b["hr"]:.0f}' + rbi = f'{b["rbi"]:.0f}' + sb = f'{b["sb"]:.0f}' + cs = f'{b["cs"]:.0f}' + so = f'{b["so"]:.0f}' + + batting_string = f'```\n' \ + f' AVG OBP SLG OPS\n' \ + f' {avg:.3f} {obp:.3f} {slg:.3f} {ops:.3f}\n``````\n' \ + f' AB R H HR RBI SB\n' \ + f'{ab: ^4} {run: ^3} {hit: ^3} {hr: >3} {rbi: >3} ' \ + f'{sb: >3}\n```' + + if p: + if p['ip'] > 0: + win = f'{p["win"]:.0f}' + loss = f'{p["loss"]:.0f}' + save = f'{p["sv"]:.0f}' + era = f'{(p["erun"] * 9) / p["ip"]:.2f}' + game = f'{p["game"]:.0f}' + gs = f'{p["gs"]:.0f}' + ip = f'{p["ip"]:.0f}' + if p["ip"] % 1 == 0: + ip += '.0' + elif str(p["ip"] % 1)[2] == '3': + ip += '.1' + else: + ip += '.2' + so = f'{p["so"]:.0f}' + whip = f'{(p["bb"] + p["hit"]) / p["ip"]:.2f}' + pitching_string = f'```\n' \ + f' W-L SV ERA IP SO WHIP\n' \ + f'{win: >2}-{loss: <2} {save: >2} {era: >5} {ip: ^5} ' \ + f'{so: ^4} {whip: >4}\n```' + + if batting_string and pitching_string: + if b['ab'] > p['ip']: + embed.add_field(name='Batting Stats', value=batting_string, inline=False) + else: + embed.add_field(name='Pitching Stats', value=pitching_string, inline=False) + elif batting_string: + embed.add_field(name='Batting Stats', value=batting_string, inline=False) + elif pitching_string: + embed.add_field(name='Pitching Stats', value=pitching_string, inline=False) + + await ctx.send(content=None, embed=embed) + + @commands.command(name='schedule', help='This week and next') + async def schedule_command(self, ctx, *week: int): + current = await get_current() + + if week: + schedule_week = week[0] + elif current['week'] < 1: + schedule_week = 1 + else: + schedule_week = current['week'] + + this_week_raw = await get_schedule( + current['season'], + week_start=schedule_week, + week_end=schedule_week + ) + next_week_raw = await get_schedule( + current['season'], + week_start=schedule_week + 1, + week_end=schedule_week + 1 + ) + results_this_week = await get_results( + current['season'], + week=schedule_week + ) + this_week = dict(sorted(this_week_raw.items(), key=lambda item: item[1]["id"])) + next_week = dict(sorted(next_week_raw.items(), key=lambda item: item[1]["id"])) + + game_count = 0 + for x in this_week: + game_count += this_week[x]['gamecount'] + + embed = get_team_embed(f'Season {current["season"]} | Week {schedule_week} | ' + f'{len(results_this_week)}/{game_count} games played') + + embed.add_field(name='Full Schedule', + value=SBA_SCHEDULE_URL, + inline=False) + + string_this_week_0 = '' + string_this_week_1 = '' + count = 0 + for x in this_week: + away_wins = 0 + away_losses = 0 + weekly_results = await get_results(current['season'], week=schedule_week, + away_abbrev=this_week[x]['awayteam']['abbrev']) + for y in weekly_results: + if weekly_results[y]['awayteam'] == this_week[x]['awayteam'] and \ + weekly_results[y]['hometeam'] == this_week[x]['hometeam']: + if weekly_results[y]['awayscore'] > weekly_results[y]['homescore']: + away_wins += 1 + else: + away_losses += 1 + + this_line = f'`({away_wins}) {this_week[x]["awayteam"]["abbrev"]: >4}` ' \ + f'{await team_emoji(ctx, this_week[x]["awayteam"])} @ ' \ + f'{await team_emoji(ctx, this_week[x]["hometeam"])} ' \ + f'`{this_week[x]["hometeam"]["abbrev"]: <4} ({away_losses})`\n' + + if count > 8: + string_this_week_1 += this_line + else: + string_this_week_0 += this_line + + count += 1 + + string_next_week_0 = '' + string_next_week_1 = '' + count = 0 + for x in next_week: + this_line = f'`{next_week[x]["awayteam"]["abbrev"]: >4}` ' \ + f'{await team_emoji(ctx, next_week[x]["awayteam"])} @ ' \ + f'{await team_emoji(ctx, next_week[x]["hometeam"])} ' \ + f'`{next_week[x]["hometeam"]["abbrev"]: <4}`\n' + + if count > 8: + string_next_week_1 += this_line + else: + string_next_week_0 += this_line + + count += 1 + + if len(string_this_week_0) > 0: + embed.add_field( + name=f'**Week {schedule_week} Games:**', + value=string_this_week_0, + inline=False + ) + await ctx.send(content=None, embed=embed) + + if len(string_this_week_1) > 0: + if len(embed.fields) > 1: + embed.remove_field(0) + embed.set_field_at( + index=0, + name=f'**Week {schedule_week} Part 2:**', + value=string_this_week_1, + inline=False + ) + await ctx.send(content=None, embed=embed) + + if len(string_next_week_0) > 0: + if len(embed.fields) > 1: + embed.remove_field(0) + embed.set_field_at( + index=0, + name=f'**Week {schedule_week + 1} Games:**', + value=string_next_week_0, + inline=False + ) + embed.title = f'Season {current["season"]} | Week {schedule_week + 1}' + await ctx.send(content=None, embed=embed) + if len(string_next_week_1) > 0: + if len(embed.fields) > 1: + embed.remove_field(0) + embed.set_field_at( + index=0, + name=f'**Week {schedule_week + 1} Part 2:**', + value=string_next_week_1, + inline=False + ) + await ctx.send(content=None, embed=embed) + + @commands.command(name='weather', help='Roll ballpark weather') + async def weather_command(self, ctx, *team_abbrev: str): + current = await get_current() + + if team_abbrev: + team = await get_one_team(team_abbrev[0]) + else: + team = await get_team_by_owner(current['season'], ctx.author.id) + + d_twenty = random.randint(1, 20) + embed = get_team_embed('Weather Chart', team, thumbnail=False) + embed.set_image(url=team['stadium']) + embed.add_field(name=f'Weather roll for {ctx.author.name}', + value=f'```md\n# {d_twenty}\nDetails:[1d20 ({d_twenty})]\n```') + + await ctx.send(content=None, embed=embed) + + @commands.command(name='standings', help='Current standings') + async def standings_command(self, ctx): + current = await get_current() + div_one = await get_standings(current['season'], league_abbrev='al', division_abbrev='e') + div_two = await get_standings(current['season'], league_abbrev='al', division_abbrev='w') + div_three = await get_standings(current['season'], league_abbrev='nl', division_abbrev='e') + div_four = await get_standings(current['season'], league_abbrev='nl', division_abbrev='w') + + div_one_standings = f'```\nTeam W-L PCT GB E# RD\n' + for team in div_one: + div_one_standings += self.team_stan_line(div_one[team]) + div_one_standings += f'\n```' + + div_two_standings = f'```\nTeam W-L PCT GB E# RD\n' + for team in div_two: + div_two_standings += self.team_stan_line(div_two[team]) + div_two_standings += f'\n```' + + div_three_standings = f'```\nTeam W-L PCT GB E# RD\n' + for team in div_three: + div_three_standings += self.team_stan_line(div_three[team]) + div_three_standings += f'\n```' + + div_four_standings = f'```\nTeam W-L PCT GB E# RD\n' + for team in div_four: + div_four_standings += self.team_stan_line(div_four[team]) + div_four_standings += f'\n```' + + progress = await self.game_progress(current) + + embed = discord.Embed(title=f'Season {current["season"]} | Week {current["week"]} | ' + f'{progress["games_played"]}/{progress["game_count"]} games played', + color=0xB70000) + embed.add_field(name=f'**Full Standings**', value=SBA_STANDINGS_URL, inline=False) + embed.add_field(name=f'**AL East**', value=div_one_standings, inline=False) + embed.add_field(name=f'**AL West**', value=div_two_standings, inline=False) + embed.add_field(name=f'**NL East**', value=div_three_standings, inline=False) + embed.add_field(name=f'**NL West**', value=div_four_standings, inline=False) + embed.set_footer(text='Run !wildcard to see wildcard standings') + + await ctx.send(content=None, embed=embed) + + @commands.command(name='wildcard', aliases=['wc'], help='Current wildcard') + async def wildcard_command(self, ctx): + current = await get_current() + + al_teams = await get_standings(current['season'], league_abbrev='al') + nl_teams = await get_standings(current['season'], league_abbrev='nl') + + al_wildcard = f'```\nTeam W-L PCT GB E# RD\n' + for team in al_teams: + al_wildcard += self.team_stan_line(al_teams[team], s_type='wc') + al_wildcard += '```' + + nl_wildcard = f'```\nTeam W-L PCT GB E# RD\n' + for team in nl_teams: + nl_wildcard += self.team_stan_line(nl_teams[team], s_type='wc') + nl_wildcard += '```' + + progress = await self.game_progress(current) + + embed = discord.Embed(title=f'Season {current["season"]} | Week {current["week"]} | ' + f'{progress["games_played"]}/{progress["game_count"]} games played', + color=0xB70000) + embed.add_field(name=f'**Full Standings**', value=SBA_STANDINGS_URL, inline=False) + embed.add_field(name=f'**AL Wildcard**', value=al_wildcard, inline=False) + embed.add_field(name=f'**NL Wildcard**', value=nl_wildcard, inline=False) + embed.set_footer(text='Run !standings to see divisional standings') + + await ctx.send(content=None, embed=embed) + + # @commands.command(name='setinjury', aliases=['clearinjury'], help='!SetInjury or !ClearInjury') + # @commands.has_any_role(SBA_PLAYERS_ROLE_NAME) + # async def set_injury_command(self, ctx, *, player_name): + # await ctx.send('This command has been deprecated. Please use `/setinjury` or `/clearinjury`') + # return + # + # current = await get_current() + # + # player = await get_one_player(player_name) + # + # # Check if player is on owner's team + # team = await get_team_by_owner(current['season'], ctx.author.id) + # if not player['team'] == team and not player['team']['abbrev'][:len(team['abbrev'])] == team['abbrev']: + # await ctx.send(f'Is this some kind of tom foolery? {player["name"]} is on {player["team"]["abbrev"]} aka ' + # f'not your team. I\'m watching you.') + # return + # + # embed = get_team_embed(f'Injury Update', team=team) + # if player['il_return']: + # # Confirm injury is over + # old_injury = player['il_return'] + # prompt = f'{player["name"]}\'s return was set for {player["il_return"]}. Is he eligible to play again?' + # this_q = Question(self.bot, ctx.channel, prompt, 'yesno', 60.0) + # resp = await this_q.ask([ctx.author]) + # if not resp: + # await ctx.send('Okay, let me know when it\'s over.') + # return + # else: + # player['il_return'] = None + # if await patch_player(player['id'], il_return=False): + # embed.add_field( + # name=f'{player["name"]}', + # value=f'{team["sname"]} {player["pos_1"]} {player["name"]}\'s injury ({old_injury}) has ended' + # ) + # # await log_channel.send( + # # f'{player["team"]["sname"]} {player["pos_1"]} {player["name"]}\'s IL stint is over.' + # # ) + # else: + # # Get injury end date + # prompt = f'When is {player["name"]} eligible to play again? (eg: w17g2)' + # this_q = Question(self.bot, ctx.channel, prompt, 'text', 60.0) + # resp = await this_q.ask([ctx.author]) + # injury_pattern = 'wk[0-9]+g[1-7]|w[0-9]+g[1-7]' + # + # if not resp: + # await ctx.send('Tell you hwat. You recount it. Math is hard. Then you can get back to me.') + # return + # elif not re.search(injury_pattern, resp.lower()): + # await ctx.send('I, uh...I was expecting something shorter like this: w12g4. Try again maybe?') + # return + # else: + # date_list = re.split('[a-z]+', resp.lower()) + # return_date = f'w{date_list[1]:0>2}g{date_list[2]}' + # player['il_return'] = return_date + # if await patch_player(player['id'], il_return=return_date): + # embed.add_field( + # name=f'{player["name"]}', + # value=f'{team["sname"]} {player["pos_1"]} {player["name"]} is injured until {return_date}' + # ) + # # await log_channel.send( + # # f'{player["team"]["sname"]} {player["pos_1"]} {player["name"]} can return {player["il_return"]}' + # # ) + # + # await ctx.send(random_salute_gif()) + # await send_to_channel(self.bot, 'sba-network-news', content=None, embed=embed) + # await self.update_injuries(ctx) + + @app_commands.command(name='setinjury', description='Set the return date for injured player') + @app_commands.guilds(discord.Object(id=os.environ.get('GUILD_ID'))) + @app_commands.describe( + player_name='Name of injured player', + this_week='The current SBa week', + this_game='The game number in which the player got injured', + inj_games='The number of games the player will miss' + ) + @app_commands.checks.has_any_role(SBA_PLAYERS_ROLE_NAME) + async def set_injury_slash( + self, interaction: discord.Interaction, player_name: str, this_week: int, this_game: int, inj_games: int): + current = await get_current() + + player = await get_one_player(player_name) + + # Check if player is on owner's team + team = await get_team_by_owner(current['season'], interaction.user.id) + if not player['team'] == team and not player['team']['abbrev'][:len(team['abbrev'])] == team['abbrev']: + await interaction.response.send_message( + f'Is this some kind of tom foolery? {player["name"]} is on {player["team"]["abbrev"]} aka ' + f'not your team. I\'m watching you.' + ) + return + + out_weeks = math.floor(inj_games / 4) + out_games = inj_games % 4 + + return_week = this_week + out_weeks + return_game = this_game + 1 + out_games + if this_game + 1 + out_games > 4: + return_week += 1 + return_game -= 4 + + return_date = f'w{return_week:>02}g{return_game:>02}' + if await patch_player(player['id'], il_return=return_date): + await patch_current(injury_count=current['injury_count'] + 1) + embed = get_team_embed(f'Injury Update', team=team) + embed.add_field( + name=f'{player["name"]}', + value=f'{team["sname"]} {player["pos_1"]} {player["name"]} is injured until {return_date}' + ) + + await log_injury( + current, {'player': player, 'type': 'new', 'current_game': this_game, 'injury_length': inj_games} + ) + await interaction.response.send_message(random_salute_gif()) + await send_to_channel(self.bot, 'sba-network-news', content=None, embed=embed) + await self.update_injuries(interaction) + else: + await interaction.response.send_message('Well that didn\'t work.') + + @app_commands.command(name='clearinjury', description='Clear the injury for a player') + @app_commands.describe(player_name='Name of injured player') + @app_commands.checks.has_any_role(SBA_PLAYERS_ROLE_NAME) + async def clear_injury_slash(self, interaction: discord.Interaction, player_name: str): + current = await get_current() + player = await get_one_player(player_name) + + if not player['il_return']: + await interaction.response.send_message('Huh? He isn\'t injured, numb nuts.') + return + + team = await get_team_by_owner(current['season'], interaction.user.id) + if not player['team'] == team and not player['team']['abbrev'][:len(team['abbrev'])] == team['abbrev']: + await interaction.response.send_message( + f'Is this some kind of tom foolery? {player["name"]} is on {player["team"]["abbrev"]} aka ' + f'not your team. I\'m watching you.' + ) + return + + old_injury = player['il_return'] + await interaction.response.send_message(f'{player["name"]}\'s return was set for {player["il_return"]}.') + view = Confirm(responders=[interaction.user]) + question = await interaction.channel.send('Is he eligible to play again?', view=view) + await view.wait() + + if view.value: + player['il_return'] = None + if await patch_player(player['id'], il_return=False): + await patch_current(injury_count=current['injury_count'] + 1) + embed = get_team_embed(f'Injury Update', team=team) + embed.add_field( + name=f'{player["name"]}', + value=f'{team["sname"]} {player["pos_1"]} {player["name"]}\'s injury ({old_injury}) has ended' + ) + + await log_injury( + current, {'player': player, 'type': 'clear', 'current_game': None, 'injury_length': None} + ) + await question.edit(content=random_conf_gif(), view=None) + await send_to_channel(self.bot, 'sba-network-news', content=None, embed=embed) + await self.update_injuries(interaction) + else: + await interaction.response.message_update('Well that didn\'t work.') + else: + await question.edit(content='You keep thinking on it.', view=None) + + # @commands.command(name='result', help='Log result') + # @commands.has_any_role(SBA_PLAYERS_ROLE_NAME) + # async def result_command( + # self, ctx, away_abbrev: str, away_score: int, home_abbrev: str, home_score: int, *week: int): + # # current = await get_current() + # # away_team = await get_one_team(away_abbrev) + # # home_team = await get_one_team(home_abbrev) + # # this_week = week[0] if week else current['week'] + # # + # # this_matchup = await get_schedule(current['season'], week_start=this_week, week_end=this_week, + # # away_abbrev=away_team["abbrev"], home_abbrev=home_team["abbrev"]) + # # these_results = await get_results(current['season'], week=this_week, away_abbrev=away_team["abbrev"], + # # home_abbrev=home_team["abbrev"]) + # # + # # # Check if scores match a weekly matchup + # # game_count = 0 + # # legal = False + # # for x in this_matchup: + # # if this_matchup[x]['awayteam'] == away_team and this_matchup[x]['hometeam'] == home_team: + # # legal = True + # # game_count = this_matchup[x]['gamecount'] + # # if not legal: + # # raise ValueError(f'I do not see a matchup between {away_team["abbrev"]} and {home_team["abbrev"]} ' + # # f'in week {this_week}') + # # + # # # Check if author is gm of these teams or the bot owner + # # if ctx.author.id not in [away_team['gmid'], away_team['gmid2'], home_team['gmid'], home_team['gmid2']] \ + # # and ctx.author.id != self.bot.owner_id: + # # await ctx.message.add_reaction('❌') + # # raise PermissionError('Only GMs of these teams can enter results') + # # + # # if len(these_results) >= game_count: + # # raise ValueError('It looks like that series is complete') + # # + # # prompt = f'Should I log {away_team["abbrev"]} {await team_emoji(ctx, away_team)} {away_score} @ ' \ + # # f'{home_score} {await team_emoji(ctx, home_team)} {home_team["abbrev"]} ' \ + # # f'as w{this_week}g{len(these_results) + 1}?' + # # this_q = Question(self.bot, ctx.channel, prompt, 'yesno', 45) + # # resp = await this_q.ask([ctx.author]) + # # + # # if not resp: + # # await ctx.send('No worries, I\'ll be here when you\'re ready.') + # # return + # # else: + # # result = { + # # 'week': this_week, + # # 'game': len(these_results) + 1, + # # 'away_team_id': away_team['id'], + # # 'home_team_id': home_team['id'], + # # 'away_score': away_score, + # # 'home_score': home_score, + # # 'season': current['season'] + # # } + # # if await post_result(result): + # # await ctx.message.add_reaction('✅') + # # await ctx.send(random_conf_gif()) + # # update = await ctx.send('I\'m tallying standings now...') + # # if await post_standings_recalc(current['season']): + # # await update.delete() + # await ctx.send(f'You no longer have to submit scores manually. They will be added when you run `/sba-submit`') + + # @commands.command(name='drive', help='Season 5 Drive') + # async def drive_command(self, ctx): + # await ctx.send('Here is a link to the Season 5 drive:\n' + # 'https://drive.google.com/drive/folders/1W8B54gZsDXXT3coKVtnGYNiYnUAPcwNB?usp=sharing') + + # @commands.command(name='rules', aliases=['rulesref', 'rulesreference', 'reference'], help='Rules reference') + # async def rules_command(self, ctx): + # await ctx.send('Here is the online rules reference: \n\n' + # 'Here is a link to the google doc:\n' + # 'https://docs.google.com/document/d/1yGZcHy9zN2MUi4hnce12dAzlFpIApbn7zR24vCkPl1o') + + @commands.command(name='links', aliases=['scorecards', 'scorecard', 'link', 'resources', 'resource'], + help='Links for league resources') + async def scorecard_command(self, ctx): + await ctx.send( + '***S C O R E C A R D S***\n' + '**OG Card**:\n\n' + '**S6 PCD**:\n\n' + '**Foxx\'s**:\n\n' + '**Josef\'s**:\n\n\n' + '***R E F E R E N C E S***\n' + '**Rules Reference**:\n\n' + '**League Guidelines**:\n\n' + '**Scouting Reference**:\n' + ) + + @commands.command(name='rest', help='Pitcher rest charts', hidden=True) + async def rest_command(self, ctx): + # sp_rest_url = 'https://lh3.googleusercontent.com/pw/ACtC-3eeRv52FZIUZNNafBBpwxJIx_OViLYHbsyi129GkJkIiv7r-X3O' \ + # 'In9_mfkr9GVl2NbLhCJ9j1IQD5DX75bLwX0gVAFExkGNolnngKck9yx6g-qjQ6QoXkc2t821UwxGoRNbZadxfd3wUn32a' \ + # 'zdQnJxnZw=w419-h703-no?authuser=0' + # rp_rest_url = 'https://lh3.googleusercontent.com/pw/ACtC-3eU_nNLYv2KLJTCom9BUSkjY-kD-VBumaiPYQxzfDB23yvkozLc' \ + # 'Qt9ai-xIwXe7CsGJ61qKgv6s6-lJp3_LRImwDFfP2VzUyPDFjE3P_CiuL6FsZCrzIBylQdLBXtxEsVmnGeSLv1WtJ--Wf' \ + # '3KzZVoOkQ=w525-h453-no?authuser=0' + # + # sp_embed = discord.Embed(title='SP Rest Chart') + # sp_embed.set_image(url=sp_rest_url) + # await ctx.send(content=None, embed=sp_embed) + # + # rp_embed = discord.Embed(title='RP Rest Chart') + # rp_embed.set_image(url=rp_rest_url) + # await ctx.send(content=None, embed=rp_embed) + # await ctx.send('For full rules, see ') + await ctx.send('This command has been deprecated - please run `/charts rest`') + + @app_commands.command(name='sba-submit', description='Submit scorecard and game result') + @app_commands.checks.has_any_role(SBA_PLAYERS_ROLE_NAME) + async def submit_slash(self, interaction: discord.Interaction, sheet_url: str): + current = await get_current() + + # Go get scorecard + await interaction.response.send_message(content='I\'ll go grab that card now...') + logging.info(f'Checking scorecard {sheet_url}') + + # Try to get card + try: + sheets = pygsheets.authorize(service_file='storage/major-domo-service-creds.json') + scorecard = sheets.open_by_url(sheet_url).worksheet_by_title('Results') + except Exception as e: + logging.error(f'Failed to access scorecard {sheet_url}: {e}') + await interaction.edit_original_response(content='Is that sheet public? I can\'t access it.') + return + + # Get outline data from the sheet + try: + away_bats = scorecard.get_values('A3', 'AK19') + home_bats = scorecard.get_values('A33', 'AK49') + away_arms = scorecard.get_values('A22', 'V30') + home_arms = scorecard.get_values('A52', 'V59') + away_team = await get_one_team(away_arms[0][1]) + home_team = await get_one_team(home_arms[0][1]) + week_num = away_bats[0][35] + game_num = home_bats[0][36] + except Exception as e: + logging.error(f'Failed to read scorecard {sheet_url}: {e}') + await interaction.edit_original_response(content=f'Oof, I got you an error:\n\n{e}.') + return + + # Confirm teams and matchup + this_matchup = await get_schedule( + current['season'], away_abbrev=away_team['abbrev'], home_abbrev=home_team['abbrev'], week_start=week_num, + week_end=week_num + ) + if len(this_matchup) != 1: + await interaction.edit_original_response( + content=f'Hmm...it doesn\'t look like {away_team["abbrev"]} played {home_team["abbrev"]} in ' + f'week {week_num}.' + ) + return + + # Confirm stats for game don't already exist + old_home_bat = await get_battingstat( + season=current['season'], team_abbrev=home_team['abbrev'], week_start=week_num, + week_end=week_num, game_num=game_num) + old_away_bat = await get_battingstat( + season=current['season'], team_abbrev=away_team['abbrev'], week_start=week_num, + week_end=week_num, game_num=game_num) + old_home_arm = await get_pitchingstat( + season=current['season'], team_abbrev=home_team['abbrev'], week_start=week_num, + week_end=week_num, game_num=game_num) + old_away_arm = await get_pitchingstat( + season=current['season'], team_abbrev=away_team['abbrev'], week_start=week_num, + week_end=week_num, game_num=game_num) + + # Confirm submitting GM + if await get_team_by_owner(current['season'], interaction.user.id) not in [home_team, away_team] and \ + interaction.user.id != self.bot.owner_id: + await interaction.edit_original_response( + content=f'{await get_emoji(interaction, "squint")} Only a GM of the two teams can submit scorecards.' + ) + return + + if len(old_home_bat) + len(old_away_bat) + len(old_home_arm) + len(old_away_arm) > 0: + view = Confirm(responders=[interaction.user]) + question = await interaction.channel.send( + f'I already see stats for {away_team["abbrev"]} @ {home_team["abbrev"]} week {week_num} game ' + f'{game_num}. Would you like me to remove those and update with this card?', + view=view + ) + await view.wait() + + if view.value: + logging.info(f'sba-submit - view.value') + this_game = { + 'season': current['season'], + 'week': week_num, + 'game_num': game_num, + 'away_team_id': away_team['id'], + 'home_team_id': home_team['id'] + } + + await question.edit( + content='What a pain in my balls. Let\'s start by deleting the batting stats...', + view=None + ) + if await delete_battingstats(this_game): + await question.edit(content='Batting stats are gone - now the pitching stats...') + + if await delete_pitchingstats(this_game): + await question.edit( + content='Pitching stats are gone - now to recalculate everybody\'s season batting lines...' + ) + + if await recalc_batting_seasons(current['season'], away_team['id']): + if await recalc_batting_seasons(current['season'], home_team['id']): + await question.edit( + content='Batting lines are done - now to recalculate pitching lines...' + ) + + if await recalc_pitching_seasons(current['season'], away_team['id']): + if await recalc_pitching_seasons(current['season'], home_team['id']): + await question.edit( + content='Pitching lines are done - now to recalculate fielding lines...' + ) + + if await recalc_fielding_seasons(current['season'], away_team['id']): + if await recalc_fielding_seasons(current['season'], home_team['id']): + await question.edit( + content=f'All done. Don\'t suck next time ' + f'{await get_emoji(interaction.guild, "lakemonsters")}' + ) + + week_games = await get_results( + current['season'], away_abbrev=away_team['abbrev'], home_abbrev=home_team['abbrev'], week=week_num + ) + this_game = None + for x in week_games: + if week_games[x]['game'] == game_num: + this_game = week_games[x] + break + if this_game: + await delete_result(this_game['id']) + + else: + logging.info(f'sba-submit - view.value "else"') + await interaction.channel.send('Alright, we\'ll call it a day here.') + return + + logging.info(f'sba-submit - reading scorecard') + # Read scorecard + errors = [] + b_counts = {'sbc': 0, 'csc': 0, 'sb': 0, 'cs': 0, 'so': 0, 'bb': 0, 'run': 0, 'rbi': 0, 'xba': 0, 'xbt': 0, + 'raa': 0, 'rto': 0} + p_counts = {'gs': 0, 'win': 0, 'loss': 0, 'so': 0, 'bb': 0, 'run': 0, 'erun': 0, 'ir': 0, 'irs': 0} + positions = ['C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'DH', 'PH', 'PR', 'P'] + pit_nums = [f'{x}' for x in range(20)] + + bat_stats = ['player', 'team', 'pos', 'pa', 'ab', 'run', 'hit', 'rbi', 'double', 'triple', 'hr', 'bb', 'so', + 'hbp', 'sac', 'ibb', 'gidp', 'sb', 'cs', 'bphr', 'bpfo', 'bp1b', 'bplo', 'xba', 'xbt', 'xch', + 'xhit', 'error', 'pb', 'sbc', 'csc', 'roba', 'robs', 'raa', 'rto', 'week', 'game'] + pit_stats = ['player', 'team', 'ip', 'hit', 'run', 'erun', 'so', 'bb', 'hbp', 'wp', 'balk', 'hr', 'ir', 'irs', + 'gs', 'win', 'loss', 'hold', 'sv', 'bsv', 'week', 'game'] + + new_bat_stats = [] + new_pit_stats = [] + final_box_away = {'r': 0, 'h': 0, 'e': 0, 'ip': 0} + final_box_home = {'r': 0, 'h': 0, 'e': 0, 'ip': 0} + final_wp = None + final_lp = None + final_holds = [] + final_sv = None + final_bsv = [] + final_doubles = [] + final_triples = [] + final_homeruns = [] + final_sbs = [] + final_cscs = [] + + # Read batting stats + for group in [away_bats, home_bats]: + for line in group: + if line[0]: + # Dictionary to hold data to be passed to db for BatStat model + this_batter = {'season': current['season']} + + # Keeping count so we can add stat name and data from sheet into this_batter dict + for count in range(len(bat_stats)): + if line[count] != '' and line[count] != '#REF' and line[count] != 0: + this_batter[bat_stats[count]] = line[count] + + # Add this_batter to collection new_bat_stats + try: + this_batter['player_id'] = self.player_list[line[0].lower()] + this_batter['team_id'] = away_team['id'] if line[1].lower() == away_team['abbrev'].lower() \ + else home_team['id'] + # Check for pitcher int in position + if this_batter['pos'] in pit_nums: + this_batter['pos'] = 'P' + this_bat_stat = BatStat.parse_obj(this_batter) + new_bat_stats.append(this_bat_stat.dict()) + + # Get stats for news-ticker + if this_batter['team_id'] == away_team['id']: + final_box_away['r'] += this_bat_stat.run + final_box_away['h'] += this_bat_stat.hit + final_box_away['e'] += this_bat_stat.error + else: + final_box_home['r'] += this_bat_stat.run + final_box_home['h'] += this_bat_stat.hit + final_box_home['e'] += this_bat_stat.error + + if this_bat_stat.double: + final_doubles.append((this_batter['player_id'], this_bat_stat.double)) + if this_bat_stat.triple: + final_triples.append((this_batter['player_id'], this_bat_stat.triple)) + if this_bat_stat.hr: + final_homeruns.append((this_batter['player_id'], this_bat_stat.hr)) + if this_bat_stat.sb: + final_sbs.append((this_batter['player_id'], this_bat_stat.sb)) + if this_bat_stat.csc: + final_cscs.append((this_batter['player_id'], this_bat_stat.csc)) + + except Exception as e: + errors.append(f'{line[0]}: {e}') + + # Read pitching stats + for group in [away_arms, home_arms]: + for line in group: + if line[0]: + # Dictionary to hold data for PitStat model + this_pitcher = {'season': current['season']} + + # Keeping count so we can add stat name and data from sheet into this_pitcher dict + for count in range(len(pit_stats)): + if line[count] != '' and line[count] != '#REF' and line[count] != 0: + this_pitcher[pit_stats[count]] = line[count] + + # Add this_pitcher to collection new_pit_stats + try: + this_pitcher['player_id'] = self.player_list[line[0].lower()] + this_pitcher['team_id'] = away_team['id'] if line[1].lower() == away_team['abbrev'].lower() \ + else home_team['id'] + this_pit_stat = PitStat.parse_obj(this_pitcher) + + # Update IP to an even third + first_digit = f'{this_pit_stat.ip % 1}'[2] + if first_digit == '0': + this_pit_stat.ip = math.floor(this_pit_stat.ip) + elif first_digit in ['1', '3']: + this_pit_stat.ip = math.floor(this_pit_stat.ip) + (1/3) + else: + this_pit_stat.ip = math.floor(this_pit_stat.ip) + (2/3) + + new_pit_stats.append(this_pit_stat.dict()) + + # Get stats for news-ticker + if this_pit_stat.win: + final_wp = this_pit_stat.player_id + if this_pit_stat.loss: + final_lp = this_pit_stat.player_id + if this_pit_stat.sv: + final_sv = this_pit_stat.player_id + if this_pit_stat.bsv: + final_bsv.append(this_pit_stat.player_id) + if this_pit_stat.hold: + final_holds.append(this_pit_stat.player_id) + if this_pitcher['team_id'] == away_team['id']: + final_box_away['ip'] += this_pit_stat.ip + else: + final_box_home['ip'] += this_pit_stat.ip + except Exception as e: + errors.append(f'{line[0]}: {e}') + + # Error checks + await interaction.edit_original_response( + content='Just finished reading the scorecard. Now to look for all of your mistakes...' + ) + for line in new_bat_stats: + b_counts['sbc'] += line['sbc'] + b_counts['csc'] += line['csc'] + b_counts['sb'] += line['sb'] + b_counts['cs'] += line['cs'] + b_counts['so'] += line['so'] + b_counts['bb'] += line['bb'] + line['ibb'] + b_counts['run'] += line['run'] + b_counts['rbi'] += line['rbi'] + b_counts['xba'] += line['xba'] + b_counts['xbt'] += line['xbt'] + b_counts['raa'] += line['raa'] + b_counts['rto'] += line['rto'] + if line['pos'] not in positions: + errors.append(f'{line["pos"]} not a valid position') + for line in new_pit_stats: + logging.info(f'line: {line}') + p_counts['gs'] += line['gs'] + p_counts['win'] += line['win'] + p_counts['loss'] += line['loss'] + p_counts['bb'] += line['bb'] + p_counts['so'] += line['so'] + p_counts['run'] += line['run'] + p_counts['erun'] += line['erun'] + p_counts['ir'] += line['ir'] + p_counts['irs'] += line['irs'] + + if b_counts['sbc'] > b_counts['sb'] + b_counts['cs']: + errors.append(f'There are {b_counts["sbc"]} stolen base attempts (SBa), but {b_counts["sb"]} SB and ' + f'{b_counts["cs"]} CS.') + if b_counts['csc'] > b_counts['cs']: + errors.append(f'There are {b_counts["sbc"]} catcher caught stealings (CSc), but ' + f'{b_counts["cs"]} baserunner CS.') + if b_counts['so'] != p_counts['so']: + errors.append(f'There are {b_counts["so"]} batter strikeouts, but {p_counts["so"]} pitcher strikeouts.') + if b_counts['bb'] != p_counts['bb']: + errors.append(f'There are {b_counts["bb"]} batter walks, but {p_counts["bb"]} pitcher.') + if b_counts['run'] != p_counts['run']: + errors.append(f'There are {b_counts["run"]} batter runs, but {p_counts["run"]} pitcher runs.') + if b_counts['rbi'] > b_counts['run']: + errors.append(f'There are {b_counts["rbi"]} runs batted in, but {b_counts["run"]} runs scored.') + if b_counts['rto'] > b_counts['raa']: + errors.append(f'There are {b_counts["rto"]} runners thrown out, but {b_counts["raa"]} runner ' + f'advance attempts.') + if b_counts['xbt'] > b_counts['xba']: + errors.append(f'There are {b_counts["xbt"]} extra bases taken, but {b_counts["xba"]} extra ' + f'base attempts.') + if p_counts['erun'] > p_counts['run']: + errors.append(f'There are {p_counts["erun"]} earned runs and {p_counts["run"]} total runs. ') + if p_counts['gs'] != 2: + errors.append(f'There should be 2 GS, but I see {p_counts["gs"]} of them.') + if p_counts['win'] != 1: + errors.append(f'There should be 1 W, but I see {p_counts["win"]} of them.') + if p_counts['loss'] != 1: + errors.append(f'There should be 1 L, but I see {p_counts["loss"]} of them.') + if p_counts['irs'] > p_counts['ir']: + errors.append(f'There are {p_counts["irs"]} inherited runners scored and {p_counts["ir"]} ' + f'inherited runners.') + + # Post errors, if any, or post stats to db + if len(errors) > 0: + error_message = '\n- '.join(errors) + await interaction.edit_original_response( + content=f'The following errors were found in your **wk{week_num}g{game_num}** scorecard. ' + f'Please resolve them and resubmit - thanks!\n\n- {error_message}' + ) + logging.error(f'Scorecard errors: {error_message}') + return + else: + bat_conf = await post_battingstats(new_bat_stats) + pit_conf = await post_pitchingstats(new_pit_stats) + + if bat_conf and pit_conf: + await interaction.edit_original_response( + content=f'You did it! I just logged the stats for **wk{week_num}g{game_num}**' + ) + + # Update last games for pitchers + for x in new_pit_stats: + this_pitcher = await get_one_player(x["player_id"]) + game_string = f'{float(x["ip"]):.2}IP w{x["week"]}g{x["game"]}' + await patch_player(this_pitcher['id'], last_game=game_string, last_game2=this_pitcher['last_game']) + else: + await interaction.edit_original_response( + content='So the stats looked okay, but I had an accident when I tried to post them. They ' + 'did not go through.' + ) + + # Post scorecard to weekly archive + card_url = f'<{SBA_BASE_URL}/scorecards/?season={current["season"]}&week={week_num}&game={game_num}' \ + f'&away_abbrev={away_team["abbrev"]}&home_abbrev={home_team["abbrev"]}>' + # try: + # archive_channel = get_channel(interaction, name=f'week-{week_num}-archive') + # logging.info(f'archive_channel: {archive_channel}') + # if not archive_channel: + # archive_channel = await create_channel( + # interaction, + # channel_name=f'week-{week_num}-archive', + # category_name=f'Season {current["season"]} Archives', + # read_send_roles=[get_role(interaction, 'Gameplay Bots')] + # ) + # + # await archive_channel.send( + # f'Game {game_num}: **{away_team["sname"]}** {await team_emoji(interaction, away_team)} ' + # f'{final_box_away["r"]} @ {final_box_home["r"]} ' + # f'{await team_emoji(interaction, home_team)} **{home_team["sname"]}**\n{card_url}') + # except Exception as e: + # await interaction.edit_original_response( + # content='Ope. I wasn\'t able to post this to the archive channel.' + # ) + + # Post box score to news-ticker + + extras = '' + if final_box_home['ip'] > 9: + extras = f' F/{math.floor(final_box_home["ip"])}' + embed = get_team_embed( + f'{away_team["sname"]} {final_box_away["r"]} @ ' + f'{final_box_home["r"]} {home_team["sname"]}{extras}', + team=away_team if final_box_away['r'] > final_box_home['r'] else home_team + ) + embed.description = f'Week {week_num} | Game {game_num}' + + embed.add_field( + name='Box Score', + value=f'```\n' + f'Team | R | H | E |\n' + f'{away_team["abbrev"]: <4} | {final_box_away["r"]: >2} | {final_box_away["h"]: >2} | ' + f'{final_box_away["e"]: >2} |\n' + f'{home_team["abbrev"]: <4} | {final_box_home["r"]: >2} | {final_box_home["h"]: >2} | ' + f'{final_box_home["e"]: >2} |\n' + f'```', + inline=False + ) + + wp = await get_one_player(final_wp) + lp = await get_one_player(final_lp) + if final_sv: + sv = await get_one_player(final_sv) + else: + sv = None + holds = [] + bsvs = [] + for x in final_holds: + holds.append(await get_one_player(x)) + for x in final_bsv: + bsvs.append(await get_one_player(x)) + + pitching_string = f'WP: {wp["name"]}\n' \ + f'LP: {lp["name"]}\n' \ + f'{"HD: " if len(holds) > 0 else ""}' + + hold_string = '' + count = 1 + for x in holds: + hold_string += f'{x["name"]}' + if count < len(holds): + hold_string += ', ' + elif len(holds) > 0: + hold_string += '\n' + count += 1 + pitching_string += hold_string + + if sv: + pitching_string += f'SV: {sv["name"]}' + + embed.add_field(name='Pitching', value=pitching_string, inline=False) + + batting_string = '' + count = 1 + if len(final_doubles) > 0: + batting_string += '2B: ' + + for x in final_doubles: + player = await get_one_player(x[0]) + batting_string += f'{player["name"]}' + + if x[1] > 1: + batting_string += f' {x[1]}' + + if count < len(final_doubles): + batting_string += ', ' + else: + batting_string += '\n' + count += 1 + + count = 1 + if len(final_triples) > 0: + batting_string += '3B: ' + + for x in final_triples: + player = await get_one_player(x[0]) + batting_string += f'{player["name"]}' + + if x[1] > 1: + batting_string += f' {x[1]}' + + if count < len(final_triples): + batting_string += ', ' + else: + batting_string += '\n' + count += 1 + + count = 1 + if len(final_homeruns) > 0: + batting_string += 'HR: ' + + for x in final_homeruns: + player = await get_one_player(x[0]) + batting_string += f'{player["name"]}' + + if x[1] > 1: + batting_string += f' {x[1]}' + + if count < len(final_homeruns): + batting_string += ', ' + else: + batting_string += '\n' + count += 1 + + if len(batting_string) > 0: + embed.add_field(name='Batting', value=batting_string, inline=False) + + baserunning_string = '' + count = 1 + if len(final_sbs) > 0: + baserunning_string += 'SB: ' + + for x in final_sbs: + player = await get_one_player(x[0]) + baserunning_string += f'{player["name"]}' + + if x[1] > 1: + baserunning_string += f' {x[1]}' + + if count < len(final_sbs): + baserunning_string += ', ' + else: + baserunning_string += '\n' + count += 1 + + count = 1 + if len(final_cscs) > 0: + baserunning_string += 'CSc: ' + + for x in final_cscs: + player = await get_one_player(x[0]) + baserunning_string += f'{player["name"]}' + + if x[1] > 1: + baserunning_string += f' {x[1]}' + + if count < len(final_cscs): + baserunning_string += ', ' + else: + baserunning_string += '\n' + count += 1 + + if len(baserunning_string) > 0: + embed.add_field(name='Baserunning', value=baserunning_string, inline=False) + + embed.add_field( + name='Scorecard', + value=f'[Website]({card_url}) / [Sheets]({sheet_url})' + ) + embed.set_footer(text='Please share the highlights!') + + await send_to_channel( + self.bot, + 'sba-network-news', + content=None, + embed=embed + ) + + result = { + 'week': week_num, + 'game': game_num, + 'away_team_id': away_team['id'], + 'home_team_id': home_team['id'], + 'away_score': final_box_away["r"], + 'home_score': final_box_home["r"], + 'season': current['season'], + 'scorecard_url': sheet_url + } + if await post_result(result): + update = await interaction.channel.send('I\'m tallying standings now...') + if await post_standings_recalc(current['season']): + await update.delete() + + # @commands.command(name='submit', help='Submit scorecard') + # @commands.has_any_role(SBA_PLAYERS_ROLE_NAME) + # async def submit_command(self, ctx, sheet_url): + # # current = await get_current() + # # + # # # Ask for confirmation to pull card + # # prompt = 'Has your opponent double-checked this card for accuracy? They get pretty whiny when you miss ' \ + # # 'a hold or an RBI. (Yes/No)' + # # this_q = Question(bot=self.bot, channel=ctx.channel, prompt=prompt, qtype='yesno', timeout=30) + # # async with ctx.typing(): + # # if not os.environ.get("TESTING"): + # # await asyncio.sleep(4) + # # resp = await this_q.ask([ctx.author]) + # # + # # if not resp: + # # await ctx.send('Not a problem at all. You folks think it over and come back to me when you\'re sure.') + # # return + # # else: + # # await ctx.message.add_reaction('👀') + # # await ctx.send('Hold my sheets, I\'m going in.') + # # + # # # Go get scorecard + # # logging.info(f'Checking scorecard {sheet_url}') + # # async with ctx.typing(): + # # # Try to get card + # # try: + # # sheets = pygsheets.authorize(service_file='storage/major-domo-service-creds.json') + # # scorecard = sheets.open_by_url(sheet_url).worksheet_by_title('Results') + # # except Exception as e: + # # logging.error(f'Failed to open scorecard {sheet_url} with this error: {e}') + # # await ctx.message.add_reaction('❌') + # # await ctx.send(f'{ctx.message.author.mention}, I can\'t access that sheet.') + # # return + # # + # # # Get outline data from the sheet + # # try: + # # away_bats = scorecard.get_values('A3', 'AK19') + # # home_bats = scorecard.get_values('A33', 'AK49') + # # away_arms = scorecard.get_values('A22', 'V29') + # # home_arms = scorecard.get_values('A52', 'V59') + # # away_team = await get_one_team(away_arms[0][1]) + # # home_team = await get_one_team(home_arms[0][1]) + # # week_num = away_bats[0][35] + # # game_num = home_bats[0][36] + # # except Exception as e: + # # logging.error(f'Failed to read scorecard {sheet_url} with this error: {e}') + # # await ctx.message.add_reaction('❌') + # # await ctx.send(f'Yikes. I ran into an error reading this card. This could be ugly:\n\n{e}') + # # return + # # + # # # Confirm teams and matchup + # # this_matchup = await get_schedule( + # # current['season'], away_abbrev=away_team['abbrev'], home_abbrev=home_team['abbrev'], week_start=week_num, + # # week_end=week_num + # # ) + # # if len(this_matchup) != 1: + # # await ctx.send(f'Hmm...it doesn\'t look like {away_team["abbrev"]} played {home_team["abbrev"]} in ' + # # f'week {week_num}.') + # # return + # # + # # # Confirm stats for game don't already exist + # # old_home_bat = await get_battingstat( + # # season=current['season'], team_abbrev=home_team['abbrev'], week_start=week_num, + # # week_end=week_num, game_num=game_num) + # # old_away_bat = await get_battingstat( + # # season=current['season'], team_abbrev=away_team['abbrev'], week_start=week_num, + # # week_end=week_num, game_num=game_num) + # # old_home_arm = await get_pitchingstat( + # # season=current['season'], team_abbrev=home_team['abbrev'], week_start=week_num, + # # week_end=week_num, game_num=game_num) + # # old_away_arm = await get_pitchingstat( + # # season=current['season'], team_abbrev=away_team['abbrev'], week_start=week_num, + # # week_end=week_num, game_num=game_num) + # # + # # if len(old_home_bat) + len(old_away_bat) + len(old_home_arm) + len(old_away_arm) > 0: + # # if len(old_home_bat) > 0: + # # await ctx.send(f'I already see batting stats for the {home_team["sname"]} from w{week_num}g{game_num}') + # # if len(old_away_bat) > 0: + # # await ctx.send(f'I already see batting stats for the {away_team["sname"]} from w{week_num}g{game_num}') + # # if len(old_home_arm) > 0: + # # await ctx.send(f'I already see pitching stats for the {home_team["sname"]} from w{week_num}g{game_num}') + # # if len(old_away_arm) > 0: + # # await ctx.send(f'I already see pitching stats for the {away_team["sname"]} from w{week_num}g{game_num}') + # # await ctx.send('Double check the week and game number and then get back to me.') + # # return + # # + # # # Confirm submitting GM + # # if await get_team_by_owner(current['season'], ctx.author.id) not in [home_team, away_team] and \ + # # ctx.author.id != self.bot.owner_id: + # # await ctx.send(f'{await get_emoji(ctx, "squint")} Only a GM of the two teams can submit scorecards.') + # # return + # # + # # # Read scorecard + # # errors = [] + # # b_counts = {'sbc': 0, 'csc': 0, 'sb': 0, 'cs': 0, 'so': 0, 'bb': 0, 'run': 0, 'rbi': 0, 'xba': 0, 'xbt': 0, + # # 'raa': 0, 'rto': 0} + # # p_counts = {'gs': 0, 'win': 0, 'loss': 0, 'so': 0, 'bb': 0, 'run': 0, 'erun': 0, 'ir': 0, 'irs': 0} + # # positions = ['C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'DH', 'PH', 'PR', 'P'] + # # pit_nums = [f'{x}' for x in range(20)] + # # + # # bat_stats = ['player', 'team', 'pos', 'pa', 'ab', 'run', 'hit', 'rbi', 'double', 'triple', 'hr', 'bb', 'so', + # # 'hbp', 'sac', 'ibb', 'gidp', 'sb', 'cs', 'bphr', 'bpfo', 'bp1b', 'bplo', 'xba', 'xbt', 'xch', + # # 'xhit', 'error', 'pb', 'sbc', 'csc', 'roba', 'robs', 'raa', 'rto', 'week', 'game'] + # # pit_stats = ['player', 'team', 'ip', 'hit', 'run', 'erun', 'so', 'bb', 'hbp', 'wp', 'balk', 'hr', 'ir', 'irs', + # # 'gs', 'win', 'loss', 'hold', 'sv', 'bsv', 'week', 'game'] + # # + # # new_bat_stats = [] + # # new_pit_stats = [] + # # final_box_away = {'r': 0, 'h': 0, 'e': 0, 'ip': 0} + # # final_box_home = {'r': 0, 'h': 0, 'e': 0, 'ip': 0} + # # final_wp = None + # # final_lp = None + # # final_holds = [] + # # final_sv = None + # # final_bsv = [] + # # final_doubles = [] + # # final_triples = [] + # # final_homeruns = [] + # # final_sbs = [] + # # final_cscs = [] + # # + # # # Read batting stats + # # for group in [away_bats, home_bats]: + # # for line in group: + # # if line[0]: + # # # Dictionary to hold data to be passed to db for BatStat model + # # this_batter = {'season': current['season']} + # # + # # # Keeping count so we can add stat name and data from sheet into this_batter dict + # # for count in range(len(bat_stats)): + # # if line[count] != '' and line[count] != '#REF' and line[count] != 0: + # # this_batter[bat_stats[count]] = line[count] + # # + # # # Add this_batter to collection new_bat_stats + # # try: + # # this_batter['player_id'] = self.player_list[line[0].lower()] + # # this_batter['team_id'] = away_team['id'] if line[1].lower() == away_team['abbrev'].lower() \ + # # else home_team['id'] + # # # Check for pitcher int in position + # # if this_batter['pos'] in pit_nums: + # # this_batter['pos'] = 'P' + # # this_bat_stat = BatStat.parse_obj(this_batter) + # # new_bat_stats.append(this_bat_stat.dict()) + # # + # # # Get stats for news-ticker + # # if this_batter['team_id'] == away_team['id']: + # # final_box_away['r'] += this_bat_stat.run + # # final_box_away['h'] += this_bat_stat.hit + # # final_box_away['e'] += this_bat_stat.error + # # else: + # # final_box_home['r'] += this_bat_stat.run + # # final_box_home['h'] += this_bat_stat.hit + # # final_box_home['e'] += this_bat_stat.error + # # + # # if this_bat_stat.double: + # # final_doubles.append((this_batter['player_id'], this_bat_stat.double)) + # # if this_bat_stat.triple: + # # final_triples.append((this_batter['player_id'], this_bat_stat.triple)) + # # if this_bat_stat.hr: + # # final_homeruns.append((this_batter['player_id'], this_bat_stat.hr)) + # # if this_bat_stat.sb: + # # final_sbs.append((this_batter['player_id'], this_bat_stat.sb)) + # # if this_bat_stat.csc: + # # final_cscs.append((this_batter['player_id'], this_bat_stat.csc)) + # # + # # except Exception as e: + # # errors.append(f'{line[0]}: {e}') + # # + # # # Read pitching stats + # # for group in [away_arms, home_arms]: + # # for line in group: + # # if line[0]: + # # # Dictionary to hold data for PitStat model + # # this_pitcher = {'season': current['season']} + # # + # # # Keeping count so we can add stat name and data from sheet into this_pitcher dict + # # for count in range(len(pit_stats)): + # # if line[count] != '' and line[count] != '#REF' and line[count] != 0: + # # this_pitcher[pit_stats[count]] = line[count] + # # + # # # Add this_pitcher to collection new_pit_stats + # # try: + # # this_pitcher['player_id'] = self.player_list[line[0].lower()] + # # this_pitcher['team_id'] = away_team['id'] if line[1].lower() == away_team['abbrev'].lower() \ + # # else home_team['id'] + # # this_pit_stat = PitStat.parse_obj(this_pitcher) + # # + # # # Update IP to an even third + # # first_digit = f'{this_pit_stat.ip % 1}'[2] + # # if first_digit == '0': + # # this_pit_stat.ip = math.floor(this_pit_stat.ip) + # # elif first_digit in ['1', '3']: + # # this_pit_stat.ip = math.floor(this_pit_stat.ip) + (1/3) + # # else: + # # this_pit_stat.ip = math.floor(this_pit_stat.ip) + (2/3) + # # + # # new_pit_stats.append(this_pit_stat.dict()) + # # + # # # Get stats for news-ticker + # # if this_pit_stat.win: + # # final_wp = this_pit_stat.player_id + # # if this_pit_stat.loss: + # # final_lp = this_pit_stat.player_id + # # if this_pit_stat.sv: + # # final_sv = this_pit_stat.player_id + # # if this_pit_stat.bsv: + # # final_bsv.append(this_pit_stat.player_id) + # # if this_pit_stat.hold: + # # final_holds.append(this_pit_stat.player_id) + # # if this_pitcher['team_id'] == away_team['id']: + # # final_box_away['ip'] += this_pit_stat.ip + # # else: + # # final_box_home['ip'] += this_pit_stat.ip + # # except Exception as e: + # # errors.append(f'{line[0]}: {e}') + # # + # # # Error checks + # # await ctx.send('Just finished reading the scorecard. Now to look for all of your mistakes...') + # # async with ctx.typing(): + # # for line in new_bat_stats: + # # b_counts['sbc'] += line['sbc'] + # # b_counts['csc'] += line['csc'] + # # b_counts['sb'] += line['sb'] + # # b_counts['cs'] += line['cs'] + # # b_counts['so'] += line['so'] + # # b_counts['bb'] += line['bb'] + line['ibb'] + # # b_counts['run'] += line['run'] + # # b_counts['rbi'] += line['rbi'] + # # b_counts['xba'] += line['xba'] + # # b_counts['xbt'] += line['xbt'] + # # b_counts['raa'] += line['raa'] + # # b_counts['rto'] += line['rto'] + # # if line['pos'] not in positions: + # # errors.append(f'{line["pos"]} not a valid position') + # # for line in new_pit_stats: + # # logging.info(f'line: {line}') + # # p_counts['gs'] += line['gs'] + # # p_counts['win'] += line['win'] + # # p_counts['loss'] += line['loss'] + # # p_counts['bb'] += line['bb'] + # # p_counts['so'] += line['so'] + # # p_counts['run'] += line['run'] + # # p_counts['erun'] += line['erun'] + # # p_counts['ir'] += line['ir'] + # # p_counts['irs'] += line['irs'] + # # + # # if b_counts['sbc'] != b_counts['sb'] + b_counts['cs']: + # # errors.append(f'There are {b_counts["sbc"]} stolen base attempts (SBa), but {b_counts["sb"]} SB and ' + # # f'{b_counts["cs"]} CS.') + # # if b_counts['csc'] != b_counts['cs']: + # # errors.append(f'There are {b_counts["sbc"]} catcher caught stealings (CSc), but ' + # # f'{b_counts["cs"]} baserunner CS.') + # # if b_counts['so'] != p_counts['so']: + # # errors.append(f'There are {b_counts["so"]} batter strikeouts, but {p_counts["so"]} pitcher strikeouts.') + # # if b_counts['bb'] != p_counts['bb']: + # # errors.append(f'There are {b_counts["bb"]} batter walks, but {p_counts["bb"]} pitcher.') + # # if b_counts['run'] != p_counts['run']: + # # errors.append(f'There are {b_counts["run"]} batter runs, but {p_counts["run"]} pitcher runs.') + # # if b_counts['rbi'] > b_counts['run']: + # # errors.append(f'There are {b_counts["rbi"]} runs batted in, but {b_counts["run"]} runs scored.') + # # if b_counts['rto'] > b_counts['raa']: + # # errors.append(f'There are {b_counts["rto"]} runners thrown out, but {b_counts["raa"]} runner ' + # # f'advance attempts.') + # # if b_counts['xbt'] > b_counts['xba']: + # # errors.append(f'There are {b_counts["xbt"]} extra bases taken, but {b_counts["xba"]} extra ' + # # f'base attempts.') + # # if p_counts['erun'] > p_counts['run']: + # # errors.append(f'There are {p_counts["erun"]} earned runs and {p_counts["run"]} total runs. ') + # # if p_counts['gs'] != 2: + # # errors.append(f'There should be 2 GS, but I see {p_counts["gs"]} of them.') + # # if p_counts['win'] != 1: + # # errors.append(f'There should be 1 W, but I see {p_counts["win"]} of them.') + # # if p_counts['loss'] != 1: + # # errors.append(f'There should be 1 L, but I see {p_counts["loss"]} of them.') + # # if p_counts['irs'] > p_counts['ir']: + # # errors.append(f'There are {p_counts["irs"]} inherited runners scored and {p_counts["ir"]} ' + # # f'inherited runners.') + # # + # # # Post errors, if any, or post stats to db + # # if len(errors) > 0: + # # error_message = '\n- '.join(errors) + # # await ctx.message.add_reaction('❌') + # # await ctx.send(f'The following errors were found in your **wk{week_num}g{game_num}** scorecard. ' + # # f'Please resolve them and resubmit - thanks!\n\n- {error_message}') + # # logging.error(f'Scorecard errors: {error_message}') + # # return + # # else: + # # bat_conf = await post_battingstats(new_bat_stats) + # # pit_conf = await post_pitchingstats(new_pit_stats) + # # + # # if bat_conf and pit_conf: + # # await ctx.message.add_reaction('✅') + # # await ctx.send(f'You did it! I just logged the stats for **wk{week_num}g{game_num}**') + # # + # # # Update last games for pitchers + # # for x in new_pit_stats: + # # this_pitcher = await get_one_player(x["player_id"]) + # # game_string = f'{float(x["ip"]):.2}IP w{x["week"]}g{x["game"]}' + # # await patch_player(this_pitcher['id'], last_game=game_string, last_game2=this_pitcher['last_game']) + # # else: + # # await ctx.send('So the stats looked okay, but I had an accident when I tried to post them. They ' + # # 'did not go through.') + # # + # # # Post scorecard to weekly archive + # # card_url = f'<{SBA_BASE_URL}/scorecards/?season={current["season"]}&week={week_num}&game={game_num}' \ + # # f'&away_abbrev={away_team["abbrev"]}&home_abbrev={home_team["abbrev"]}>' + # # archive_channel = discord.utils.get(ctx.guild.text_channels, name=f'week-{week_num}-archive') + # # if not archive_channel: + # # overwrites = {ctx.guild.default_role: discord.PermissionOverwrite(send_messages=False, embed_links=False), + # # ctx.guild.me: discord.PermissionOverwrite(send_messages=True)} + # # archive_channel = await ctx.guild.create_text_channel( + # # f'week-{week_num}-archive', + # # overwrites=overwrites, + # # category=discord.utils.get(ctx.guild.categories, name=f'Season {current["season"]} Archives'), + # # position=0 + # # ) + # # + # # await archive_channel.send( + # # f'Game {game_num}: **{away_team["sname"]}** {await team_emoji(ctx, away_team)} ' + # # f'{final_box_away["r"]} @ {final_box_home["r"]} ' + # # f'{await team_emoji(ctx, home_team)} **{home_team["sname"]}**\n{card_url}') + # # + # # # Post box score to news-ticker + # # extras = '' + # # if final_box_home['ip'] > 9: + # # extras = f' F/{math.floor(final_box_home["ip"])}' + # # embed = get_team_embed( + # # f'{away_team["sname"]} {final_box_away["r"]} @ ' + # # f'{final_box_home["r"]} {home_team["sname"]}{extras}', + # # team=away_team if final_box_away['r'] > final_box_home['r'] else home_team + # # ) + # # embed.description = f'Week {week_num} | Game {game_num}' + # # + # # embed.add_field( + # # name='Box Score', + # # value=f'```\n' + # # f'Team | R | H | E |\n' + # # f'{away_team["abbrev"]: <4} | {final_box_away["r"]: >2} | {final_box_away["h"]: >2} | ' + # # f'{final_box_away["e"]: >2} |\n' + # # f'{home_team["abbrev"]: <4} | {final_box_home["r"]: >2} | {final_box_home["h"]: >2} | ' + # # f'{final_box_home["e"]: >2} |\n' + # # f'```', + # # inline=False + # # ) + # # + # # wp = await get_one_player(final_wp) + # # lp = await get_one_player(final_lp) + # # if final_sv: + # # sv = await get_one_player(final_sv) + # # else: + # # sv = None + # # holds = [] + # # bsvs = [] + # # for x in final_holds: + # # holds.append(await get_one_player(x)) + # # for x in final_bsv: + # # bsvs.append(await get_one_player(x)) + # # + # # pitching_string = f'WP: {wp["name"]}\n' \ + # # f'LP: {lp["name"]}\n' \ + # # f'{"HD: " if len(holds) > 0 else ""}' + # # + # # hold_string = '' + # # count = 1 + # # for x in holds: + # # hold_string += f'{x["name"]}' + # # if count < len(holds): + # # hold_string += ', ' + # # elif len(holds) > 0: + # # hold_string += '\n' + # # count += 1 + # # pitching_string += hold_string + # # + # # if sv: + # # pitching_string += f'SV: {sv["name"]}' + # # + # # embed.add_field(name='Pitching', value=pitching_string, inline=False) + # # + # # batting_string = '' + # # count = 1 + # # if len(final_doubles) > 0: + # # batting_string += '2B: ' + # # + # # for x in final_doubles: + # # player = await get_one_player(x[0]) + # # batting_string += f'{player["name"]}' + # # + # # if x[1] > 1: + # # batting_string += f' {x[1]}' + # # + # # if count < len(final_doubles): + # # batting_string += ', ' + # # else: + # # batting_string += '\n' + # # count += 1 + # # + # # count = 1 + # # if len(final_triples) > 0: + # # batting_string += '3B: ' + # # + # # for x in final_triples: + # # player = await get_one_player(x[0]) + # # batting_string += f'{player["name"]}' + # # + # # if x[1] > 1: + # # batting_string += f' {x[1]}' + # # + # # if count < len(final_triples): + # # batting_string += ', ' + # # else: + # # batting_string += '\n' + # # count += 1 + # # + # # count = 1 + # # if len(final_homeruns) > 0: + # # batting_string += 'HR: ' + # # + # # for x in final_homeruns: + # # player = await get_one_player(x[0]) + # # batting_string += f'{player["name"]}' + # # + # # if x[1] > 1: + # # batting_string += f' {x[1]}' + # # + # # if count < len(final_homeruns): + # # batting_string += ', ' + # # else: + # # batting_string += '\n' + # # count += 1 + # # + # # if len(batting_string) > 0: + # # embed.add_field(name='Batting', value=batting_string, inline=False) + # # + # # baserunning_string = '' + # # count = 1 + # # if len(final_sbs) > 0: + # # baserunning_string += 'SB: ' + # # + # # for x in final_sbs: + # # player = await get_one_player(x[0]) + # # baserunning_string += f'{player["name"]}' + # # + # # if x[1] > 1: + # # baserunning_string += f' {x[1]}' + # # + # # if count < len(final_sbs): + # # baserunning_string += ', ' + # # else: + # # baserunning_string += '\n' + # # count += 1 + # # + # # count = 1 + # # if len(final_cscs) > 0: + # # baserunning_string += 'CSc: ' + # # + # # for x in final_cscs: + # # player = await get_one_player(x[0]) + # # baserunning_string += f'{player["name"]}' + # # + # # if x[1] > 1: + # # baserunning_string += f' {x[1]}' + # # + # # if count < len(final_cscs): + # # baserunning_string += ', ' + # # else: + # # baserunning_string += '\n' + # # count += 1 + # # + # # if len(baserunning_string) > 0: + # # embed.add_field(name='Baserunning', value=baserunning_string, inline=False) + # # + # # embed.add_field(name='Scorecard', value=card_url) + # # embed.set_footer(text='Please share the highlights!') + # # + # # await send_to_channel( + # # self.bot, + # # 'sba-network-news', + # # content=None, + # # embed=embed + # # ) + # await ctx.send(f'Please run `/sba-submit` to submit scorecards.') + + # @commands.command(name='removestats', help='Remove scorecard') + # @commands.has_any_role(SBA_PLAYERS_ROLE_NAME) + # async def remove_stats_command(self, ctx, away_abbrev, home_abbrev, week_num, game_num): + # current = await get_current() + # + # owner_team = await get_team_by_owner(current['season'], ctx.author.id) + # away_team = await get_one_team(away_abbrev) + # home_team = await get_one_team(home_abbrev) + # + # # Confirm teams and matchup + # this_matchup = await get_schedule( + # current['season'], away_abbrev=away_team['abbrev'], home_abbrev=home_team['abbrev'], week_start=week_num, + # week_end=week_num + # ) + # if len(this_matchup) != 1: + # await ctx.send(f'Hmm...it doesn\'t look like {away_team["abbrev"]} played {home_team["abbrev"]} in ' + # f'week {week_num}.') + # return + # + # # Confirm stats for game don't already exist + # s_type = 'Regular' if current['week'] < current['playoffs_begin'] else 'Post' + # old_home_bat = await get_battingstat( + # season=current['season'], s_type=s_type, team_abbrev=home_team['abbrev'], week_start=week_num, + # week_end=week_num, game_num=game_num) + # old_away_bat = await get_battingstat( + # season=current['season'], s_type=s_type, team_abbrev=away_team['abbrev'], week_start=week_num, + # week_end=week_num, game_num=game_num) + # old_home_arm = await get_pitchingstat( + # season=current['season'], s_type=s_type, team_abbrev=home_team['abbrev'], week_start=week_num, + # week_end=week_num, game_num=game_num) + # old_away_arm = await get_pitchingstat( + # season=current['season'], s_type=s_type, team_abbrev=away_team['abbrev'], week_start=week_num, + # week_end=week_num, game_num=game_num) + # + # if len(old_home_bat) + len(old_away_bat) + len(old_home_arm) + len(old_away_arm) == 0: + # await ctx.send('Good news: I don\'t see any stats for that game. Bad news: you seemed to think there ' + # 'were some.') + # return + # + # # Confirm submitting GM + # if owner_team not in [home_team, away_team] and ctx.author.id != self.bot.owner_id: + # await ctx.send(f'{await get_emoji(ctx, "squint")} Only a GM of the two teams can remove stats.') + # return + # + # prompt = 'Are you sure? This will snap these stats out of existence.' + # this_q = Question(self.bot, ctx.channel, prompt, 'yesno', 30) + # resp = await this_q.ask([ctx.author]) + # + # if not resp: + # await ctx.send('Whew. There for a second I thought you fucked up a scorecard. I\'ll leave this one be.') + # return + # + # this_game = { + # 'season': current['season'], + # 'week': week_num, + # 'game_num': game_num, + # 'away_team_id': away_team['id'], + # 'home_team_id': home_team['id'] + # } + # + # await ctx.send('What a pain in my balls. Let\'s start by deleting the batting stats.') + # async with ctx.typing(): + # if await delete_battingstats(this_game): + # await ctx.send('Batting stats are gone - now the pitching stats.') + # + # async with ctx.typing(): + # if await delete_pitchingstats(this_game): + # await ctx.send('Pitching stats are gone - now to recalculate everybody\'s season batting lines.') + # + # async with ctx.typing(): + # if await recalc_batting_seasons(current['season'], away_team['id']): + # if await recalc_batting_seasons(current['season'], home_team['id']): + # await ctx.send('Batting lines are done - now to recalculate pitching lines.') + # + # async with ctx.typing(): + # if await recalc_pitching_seasons(current['season'], away_team['id']): + # if await recalc_pitching_seasons(current['season'], home_team['id']): + # await ctx.send('Pitching lines are done - now to recalculate fielding lines.') + # + # async with ctx.typing(): + # if await recalc_fielding_seasons(current['season'], away_team['id']): + # await recalc_fielding_seasons(current['season'], home_team['id']) + # + # await ctx.send(f'All done. Don\'t suck next time {await get_emoji(ctx, "lakemonsters")}') + + @app_commands.command(name='branding', description='Update your team branding') + @app_commands.guilds(discord.Object(id=os.environ.get('GUILD_ID'))) + @app_commands.rename( + team_image_url='team_logo_url', + mil_team_image_url='minor_league_logo_url', + ) + @app_commands.describe( + color_hex='Color hex code to use for your team', + team_image_url='URL ending in .png or .jpg of your team logo', + mil_color_hex='Color hex code to use for your minor league team', + mil_team_image_url='URL ending in .png or .jpg of your minor league team logo', + dice_color_hex='Color hex code to use for your dice rolls', + ) + @app_commands.checks.has_any_role(SBA_PLAYERS_ROLE_NAME) + async def branding_slash_command( + self, interaction: discord.Interaction, color_hex: str = None, team_image_url: str = None, + mil_color_hex: str = None, mil_team_image_url: str = None, dice_color_hex: str = None): + current = await get_current() + team = await get_team_by_owner(current['season'], interaction.user.id) + mil_team = await get_one_team(f'{team["abbrev"]}MiL', current['season']) + team_role = get_team_role(interaction, team) + errors = [] + show_mil = False + show_dice = False + + if not team or not team_role: + await interaction.response.send_message( + f'Do you have a team here? You don\'t look familiar. {await get_emoji(interaction, "realeyes")}' + ) + return + + if color_hex is not None: + try: + color_int = int(color_hex, 16) + await team_role.edit(colour=color_int) + await patch_team(team, color=color_hex) + except Exception as e: + logging.info(f'Error setting {team["sname"]} color to {color_hex}') + errors.append(f'- Error setting {team["sname"]} color to {color_hex}:\n{e}\n\n') + if team_image_url is not None: + if requests.get(team_image_url, timeout=0.5).status_code != 200: + errors.append(f'- I wasn\'t able to pull that image. Was it a public URL?\n\n') + else: + await patch_team(team, thumbnail=team_image_url) + if mil_color_hex is not None: + try: + await patch_team(mil_team, color=color_hex) + show_mil = True + except Exception as e: + logging.info(f'Error setting {team["sname"]} color to {color_hex}') + errors.append(f'- Error setting {team["sname"]} color to {color_hex}:\n{e}\n\n') + if mil_team_image_url is not None: + if requests.get(team_image_url, timeout=0.5).status_code != 200: + errors.append(f'- I wasn\'t able to pull that image. Was it a public URL?\n\n') + else: + await patch_team(mil_team, thumbnail=mil_team_image_url) + show_mil = True + if dice_color_hex is not None: + try: + await patch_team(team, dice_color=dice_color_hex) + show_dice = True + except Exception as e: + logging.info(f'Error setting {team["sname"]} color to {color_hex}') + errors.append(f'- Error setting {team["sname"]} color to {color_hex}:\n{e}\n\n') + + team = await get_one_team(team['id'], season=current['season']) + major_embed = get_team_embed(f'{team["lname"]} Test', team=team) + major_embed.add_field( + name='Little Test Data', + value='Run the command again if you want to change anything!' + ) + embeds = [major_embed] + + if show_mil: + mil_team = await get_one_team(mil_team['id']) + mil_embed = get_team_embed(f'{mil_team["lname"]} Test', team=mil_team) + mil_embed.add_field( + name='Little Test Data', + value='Run the command again if you want to change anything!' + ) + embeds.append(mil_embed) + + if show_dice: + logging.info(f'entering show_dice') + team['color'] = team['dice_color'] + dice_embed = get_team_embed(f'{team["lname"]} Dice Test', team=team) + logging.info(f'got base embed: {dice_embed}') + dice_embed.add_field( + name='Little Test Data', + value='This is what we\'ve got for your dice rolls!' + ) + logging.info(f'done with embed: {dice_embed}') + embeds.append(dice_embed) + + await interaction.response.send_message(content=None, embeds=embeds) + + # @commands.command(name='setcolor', help='Set team color') + # @commands.has_any_role(SBA_PLAYERS_ROLE_NAME) + # async def set_color_command(self, ctx, color_code): + # current = await get_current() + # team = await get_team_by_owner(current['season'], ctx.author.id) + # team_role = get_team_role(ctx, team) + # color_int = int(color_code, 16) + # + # if not team or not team_role: + # await ctx.send(f'Do you have a team here? You don\'t look familiar. {await get_emoji(ctx, "realeyes")}') + # return + # + # await team_role.edit(colour=color_int) + # await patch_team(team, color=color_code) + # team = await get_team_by_owner(current['season'], ctx.author.id) + # + # embed = get_team_embed(f'{team["lname"]} Test', team=team) + # embed.add_field( + # name='Reminder', + # value='Don\'t forget to set your team thumbnail image with the !setthumbnail command!' + # ) + # await ctx.send(content='Got it! What do you think?', embed=embed) + # + # @commands.command(name='setthumbnail', help='Set team pic') + # @commands.has_any_role(SBA_PLAYERS_ROLE_NAME) + # async def set_thumbnail_command(self, ctx, thumbnail_url): + # current = await get_current() + # team = await get_team_by_owner(current['season'], ctx.author.id) + # team_role = get_team_role(ctx, team) + # + # if not team or not team_role: + # await ctx.send(f'Do you have a team here? You don\'t look familiar. {await get_emoji(ctx, "realeyes")}') + # return + # + # if requests.get(thumbnail_url, timeout=0.5).status_code != 200: + # await ctx.send('I wasn\'t able to pull that thumbnail. Was it a public URL?') + # + # await patch_team(team, thumbnail=thumbnail_url) + # team = await get_team_by_owner(current['season'], ctx.author.id) + # + # embed = get_team_embed(f'{team["lname"]} Test', team=team) + # embed.add_field( + # name='Reminder', + # value='Don\'t forget to set your team color with the !setcolor command!' + # ) + # + # await ctx.send(content='Got it! What do you think?', embed=embed) + # + # @commands.command(name='setminorthumbnail', help='Set minor team pic') + # @commands.has_any_role(SBA_PLAYERS_ROLE_NAME) + # async def set_minor_pic_command(self, ctx, thumbnail_url): + # current = await get_current() + # team = await get_team_by_owner(current['season'], ctx.author.id) + # minor_team = await get_one_team(team['id'] + 2) + # + # if not minor_team: + # await ctx.send(f'Do you have a team here? You don\'t look familiar. {await get_emoji(ctx, "realeyes")}') + # return + # + # if requests.get(thumbnail_url, timeout=0.5).status_code != 200: + # await ctx.send('I wasn\'t able to pull that thumbnail. Was it a public URL?') + # + # await patch_team(minor_team, thumbnail=thumbnail_url) + # minor_team = await get_one_team(team['id'] + 2) + # + # embed = get_team_embed(f'{minor_team["sname"]} Test', team=minor_team) + # embed.add_field( + # name='Reminder', + # value='Don\'t forget to set your team color with the !setminorcolor command!' + # ) + # await ctx.send(content='Got it! What do you think?', embed=embed) + # + # @commands.command(name='setminorcolor', help='Set minor team color') + # @commands.has_any_role(SBA_PLAYERS_ROLE_NAME) + # async def set_minor_color_command(self, ctx, color_code): + # current = await get_current() + # team = await get_team_by_owner(current['season'], ctx.author.id) + # minor_team = await get_one_team(team['id'] + 2) + # + # if not minor_team: + # await ctx.send(f'Do you have a team here? You don\'t look familiar. {await get_emoji(ctx, "realeyes")}') + # return + # + # await patch_team(minor_team, color=color_code) + # minor_team = await get_one_team(team['id'] + 2) + # team = await get_team_by_owner(current['season'], ctx.author.id) + # + # embed = get_team_embed(f'{minor_team["sname"]} Test', team=minor_team) + # embed.add_field( + # name='Reminder', + # value='Don\'t forget to set your team thumbnail image with the !setminorthumbnail command!' + # ) + # await ctx.send(content='Got it! What do you think?', embed=embed) + # + # @commands.command(name='setdicecolor', help='Set dice embed color') + # @commands.has_any_role(SBA_PLAYERS_ROLE_NAME) + # async def set_dice_color_command(self, ctx, color_code): + # current = await get_current() + # team = await get_team_by_owner(current['season'], ctx.author.id) + # + # await patch_team(team, dice_color=color_code) + # embed = discord.Embed( + # title=f'Dice Color: {color_code}', + # color=int(color_code, 16) + # ) + # embed.add_field( + # name='Reminder', + # value='Any dice rolled in your home stadium will use this color!' + # ) + # await ctx.send(content=None, embed=embed) + + @commands.command(name='picks', aliases=['mypicks', 'draftpicks'], help='See your picks') + async def picks_command(self, ctx, *team_abbrev): + await ctx.send('Go away.') + return + current = await get_current() + + if team_abbrev: + team = await get_one_team(team_abbrev[0]) + else: + team = await get_team_by_owner(current['season'], ctx.author.id) + + if not team: + await ctx.send('I don\'t know what team you\'re looking for.') + return + + raw_picks = await get_draftpicks(season=5, owner_team=team, team_season=5) + logging.info(f'raw_picks: {raw_picks}') + try: + all_picks = dict(sorted(raw_picks.items(), key=lambda item: item[1]["overall"])) + except TypeError as e: + all_picks = dict(sorted(raw_picks.items(), key=lambda item: item[1]["round"])) + pick_string = '' + + for x in all_picks: + squiggles = '~~' if all_picks[x]['player'] else '' + selection = f'- {all_picks[x]["player"]["name"]}' if all_picks[x]["player"] else '' + overall = f'#{all_picks[x]["overall"]} - ' if all_picks[x]["overall"] else '' + pick_string += f'{squiggles}{overall}{all_picks[x]["origowner"]["abbrev"]} {all_picks[x]["round"]}' \ + f'{squiggles} {selection}\n' + + embed = get_team_embed(f'{team["lname"]}', team=team) + embed.add_field(name=f'Season {current["season"]} Draft Picks', value=pick_string) + + await ctx.send(content=None, embed=embed) + + # @commands.command(name='fixscore', help='Change result') + # @commands.has_any_role(SBA_PLAYERS_ROLE_NAME) + # async def fix_score_command( + # self, ctx, away_team, away_score: int, home_team, home_score: int, week_num: int, game_num: int): + # current = await get_current() + # week_games = await get_results(current['season'], away_abbrev=away_team, home_abbrev=home_team, week=week_num) + # this_game = None + # + # for x in week_games: + # if week_games[x]['game'] == game_num: + # this_game = week_games[x] + # break + # + # if this_game is None: + # await ctx.send('I don\'t see a record of that game. Make sure you\'ve got the week and game number right. ' + # 'If so, go ahead and log it with the !result command') + # return + # + # if this_game['awayscore'] == away_score and this_game['homescore'] == home_score: + # await ctx.send('Good news, everyone! That is already the score I have listed!') + # return + # + # await ctx.send(f'I\'ve got that game listed as {this_game["awayteam"]["abbrev"]} ' + # f'{await team_emoji(ctx, this_game["awayteam"])} {this_game["awayscore"]} @ ' + # f'{this_game["homescore"]} {await team_emoji(ctx, this_game["hometeam"])} ' + # f'{this_game["hometeam"]["abbrev"]}') + # prompt = f'Would you like to change this to {this_game["awayteam"]["abbrev"]} ' \ + # f'{await team_emoji(ctx, this_game["awayteam"])} {away_score} @ ' \ + # f'{home_score} {await team_emoji(ctx, this_game["hometeam"])} {this_game["hometeam"]["abbrev"]}?' + # this_q = Question(self.bot, ctx.channel, prompt, 'yesno', 30) + # resp = await this_q.ask([ctx.author]) + # + # if not resp: + # await ctx.send('Mmkay. I\'ll leave it alone for now.') + # return + # else: + # if await patch_result(this_game['id'], away_score=away_score, home_score=home_score): + # await ctx.message.add_reaction('✅') + # await ctx.send(f'{get_team_role(ctx, this_game["awayteam"]).mention} ' + # f'{get_team_role(ctx, this_game["hometeam"]).mention}') + # await ctx.send(random_conf_gif()) + + @commands.command(name='newaward', help='Grant award') + @commands.has_any_role('Awards Team', 'Da Commish') + async def new_award_command(self, ctx, *, award_name): + # f'{"In-Season" if in_or_off.lower() == "in" else "Off-Season"}' + current = await get_current() + award = { + 'name': award_name, + 'season': None, + 'timing': None, + 'manager1': None, + 'manager2': None, + 'player': None, + 'team': None, + 'image': None, + 'invalid': False + } + + async def add_recipient(search_string): + try: + this_team = await get_one_team(search_string, season=award['season']) + award['team'] = this_team + except ValueError: + try: + this_manager = await get_one_manager(search_string) + if not award['manager1']: + award['manager1'] = this_manager + else: + award['manager2'] = this_manager + except ValueError: + try: + search_name = await fuzzy_player_search( + ctx, ctx.channel, self.bot, search_string, self.player_list) + this_player = await get_one_player(search_name, season=award['season']) + award['player'] = this_player + except ValueError: + try: + this_player = await get_one_player(search_string, season=award['season']) + award['player'] = this_player + except ValueError: + await ctx.send(f'I do not recognize **{search_string}**. Will you check the spelling and ' + f'try again?') + + def get_embed(): + this_embed = discord.Embed(title=award['name']) + this_embed.description = f'{award["timing"]} - S{award["season"]}' + if award['manager1']: + this_embed.add_field(name='Manager', value=award['manager1']) + if award['manager2']: + this_embed.add_field(name='Manager', value=award['manager2']) + if award['player']: + this_embed.add_field(name='Player', value=f'{award["player"]["name"]}') + if award['team']: + this_embed.add_field(name='Team', value=f'{award["team"]["lname"]}') + if award['image']: + try: + this_embed.set_image(url=award['image']) + except: + award['invalid'] = True + this_embed.add_field(name='Image', value='Invalid URL') + return this_embed + + prompt = f'Is this for season {current["season"]}?' + this_q = Question(self.bot, ctx.channel, prompt, 'yesno', timeout=45) + resp = await this_q.ask([ctx.author]) + + if resp is None: + await ctx.send('Think on it. Get back to me.') + return + elif resp: + award['season'] = current['season'] + else: + prompt = 'Please enter the season for this award.' + this_q = Question(self.bot, ctx.channel, prompt, 'int', timeout=45) + resp = await this_q.ask([ctx.author]) + + if not resp: + await ctx.send('Think on it. Get back to me.') + return + else: + award['season'] = resp + + prompt = f'Is this an In-Season award (as opposed to off-season)?' + this_q = Question(self.bot, ctx.channel, prompt, 'yesno', timeout=45) + resp = await this_q.ask([ctx.author]) + + if resp is None: + await ctx.send('Think on it. Get back to me.') + return + elif resp: + award['timing'] = 'In-Season' + else: + award['timing'] = 'Off-Season' + await ctx.send('Got it - I\'ll put this down as an Off-Season award') + + prompt = 'Is this the start you wanted?' + this_q = Question(self.bot, ctx.channel, prompt, 'yesno', timeout=45, embed=get_embed()) + resp = await this_q.ask([ctx.author]) + + if not resp: + await ctx.send('No worries - run `!newaward Award_Name` again to rename it.') + return + + # Get team/player/managers + while True: + prompt = 'Please enter the team abbreviation, player name, or manager name of the recipient.' + this_q = Question(self.bot, ctx.channel, prompt, 'text', timeout=45) + resp = await this_q.ask([ctx.author]) + + if resp is None: + await ctx.send('You think on it and hit me up when you\'re ready.') + return + else: + await add_recipient(resp) + + await ctx.send('Here is the current award', embed=get_embed()) + + prompt = 'Would you like to (re)enter a recipient?' + this_q = Question(self.bot, ctx.channel, prompt, 'yesno', timeout=45) + resp = await this_q.ask([ctx.author]) + + if resp is None: + await ctx.send('We can hold off on this for now. Let me know when you\'re ready to start again.') + return + elif not resp: + break + + # Get image URL + while True: + prompt = 'Would you like to (re)enter an image URL for this award?' + this_q = Question(self.bot, ctx.channel, prompt, 'yesno', timeout=45, embed=get_embed()) + resp = await this_q.ask([ctx.author]) + + if not resp: + break + else: + prompt = 'Please enter the image URL.' + this_q = Question(self.bot, ctx.channel, prompt, 'url', timeout=45) + resp = await this_q.ask([ctx.author]) + + if resp is None: + await ctx.send('Okey doke, nevermind. I get it. It\'s fine. We only did all this work for nothing. ' + 'Let me know when you want to start again.') + return + elif not resp: + await ctx.send(f'**{resp}** is not a valid URL.') + else: + award['image'] = resp + + prompt = 'Would you like to submit this award?' + this_q = Question(self.bot, ctx.channel, prompt, 'yesno', timeout=45, embed=get_embed()) + resp = await this_q.ask([ctx.author]) + + if not resp: + await ctx.send('Really? After all the ti- nevermind. Fine. Kill it. It\'s over. Bye.') + return + else: + await post_award(award) + await ctx.send(random_conf_gif()) + + @commands.command(name='record', help='Record through week num') + async def record_command(self, ctx, team_abbrev, week_num: int): + this_team = await get_one_team(team_abbrev) + this_record = await get_team_record(this_team, week_num) + + await ctx.send(f'The {this_team["sname"]} were ({this_record["w"]}-{this_record["l"]}) through ' + f'week {week_num}.') + + @commands.command(name='vc', aliases=['voice', 'gameplay'], help='Get voice channel') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, 'Paper Dynasty Players') + async def voice_channel_command(self, ctx): + prompt = 'Would you like to get a gameplay channel?' + this_q = Question(self.bot, ctx.channel, prompt, qtype='yesno', timeout=15) + resp = await this_q.ask([ctx.author]) + + if not resp: + await ctx.send('Fine. I bet they didn\'t wanna talk to you anyway.') + return + else: + overwrites = {ctx.guild.default_role: discord.PermissionOverwrite(speak=True)} + channel_name = f'Gameplay {random_codename()}' + this_vc = await ctx.guild.create_voice_channel( + channel_name, + overwrites=overwrites, + category=discord.utils.get(ctx.guild.categories, name=f'Voice Channels') + ) + logging.info(f'Just created voice channel: {channel_name} for {ctx.author}') + + await ctx.send(f'Just created {this_vc} for you!') + + while True: + await asyncio.sleep(900) + if len(this_vc.members) == 0: + await this_vc.delete() + logging.info(f'Just deleted voice channel: {channel_name}') + break + + @commands.command(name='private', help='Get private vc') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME) + async def private_vc_command(self, ctx): + current = await get_current() + this_team = await get_team_by_owner(current['season'], ctx.author.id) + + async def get_other_team(): + prompt = f'Please enter the abbrev of the team you are playing.' + this_q = Question(self.bot, ctx.channel, prompt, qtype='text', timeout=15) + resp = await this_q.ask([ctx.author]) + + if not resp: + await ctx.send('You keep thinking about it and hit me up later if you figure it out.') + return None + else: + other_team = await get_one_team(resp) + if not other_team: + await ctx.send(f'What\'s a **{resp}**? If you could go ahead and run this command again, that\'d ' + f'be great.') + return None + else: + return other_team + + if not this_team: + await ctx.send('Hmm...I can\'t find your team. Are you from around here?') + return + + try: + this_matchup = await get_one_schedule( + season=current['season'], + team_abbrev1=this_team['abbrev'], + week=current['week'] + ) + except ValueError as e: + other_team = await get_other_team() + if not other_team: + return + channel_name = f'{this_team["abbrev"]} vs {other_team["abbrev"]} Muted' + else: + if this_team == this_matchup['awayteam']: + other_team = this_matchup['hometeam'] + else: + other_team = this_matchup['awayteam'] + channel_name = f'{this_matchup["awayteam"]["sname"]} @ {this_matchup["hometeam"]["sname"]} Muted' + + prompt = f'Would you like to get a private voice channel for {this_team["abbrev"]} and {other_team["abbrev"]}?' + this_q = Question(self.bot, ctx.channel, prompt, qtype='yesno', timeout=15) + resp = await this_q.ask([ctx.author]) + + if resp is None: + await ctx.send('Fine. I bet they didn\'t wanna talk to you anyway.') + return + elif not resp: + other_team = await get_other_team() + if not other_team: + return + channel_name = f'{this_team["abbrev"]} vs {other_team["abbrev"]}' + + this_team_role = discord.utils.get(ctx.guild.roles, name=f'{this_team["lname"]}') + other_team_role = discord.utils.get(ctx.guild.roles, name=f'{other_team["lname"]}') + + overwrites = {ctx.guild.default_role: discord.PermissionOverwrite(speak=False), + this_team_role: discord.PermissionOverwrite(speak=True), + other_team_role: discord.PermissionOverwrite(speak=True)} + this_vc = await ctx.guild.create_voice_channel( + channel_name, + overwrites=overwrites, + category=discord.utils.get(ctx.guild.categories, name=f'Voice Channels') + ) + + await ctx.send(f'Just created {this_vc} for you!') + + while True: + await asyncio.sleep(900) + if len(this_vc.members) == 0: + await this_vc.delete() + break + + @commands.command(name='headshot', aliases=['hs'], help='Set headshot pic') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME) + async def headshot_command(self, ctx, url, *, player_name): + if ctx.channel.name == 'season-6-chat': + await ctx.send('Not in season 6 chat, dumbass.') + return + + current = await get_current() + this_team = await get_team_by_owner(current['season'], ctx.author.id) + + if not this_team: + await ctx.send('Hmm...I can\'t find your team. Are you from around here?') + return + + player = await get_one_player( + await fuzzy_player_search(ctx, ctx.channel, self.bot, player_name, self.player_list.keys()) + ) + + player_embed = await get_player_embed(player, current) + player_embed.set_thumbnail(url=url) + prompt = 'Would you like to save this image?' + this_q = Question(self.bot, ctx.channel, prompt, 'yesno', 15, player_embed) + resp = await this_q.ask([ctx.author]) + + if not resp: + await ctx.send('That\'s okay. It wasn\'t a very good picture anyway.') + return + else: + await patch_player(player['id'], headshot=url) + await ctx.send(random_conf_word()) + + @commands.command(name='fancycard', aliases=['fc'], help='Set vanity card') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME) + async def vanity_card_command(self, ctx, url, *, player_name): + if ctx.channel.name == 'season-6-chat': + await ctx.send('Not in season 6 chat, dumbass.') + return + + current = await get_current() + this_team = await get_team_by_owner(current['season'], ctx.author.id) + + if not this_team: + await ctx.send('Hmm...I can\'t find your team. Are you from around here?') + return + + player = await get_one_player( + await fuzzy_player_search(ctx, ctx.channel, self.bot, player_name, self.player_list.keys()) + ) + + player_embed = await get_player_embed(player, current) + player_embed.set_thumbnail(url=url) + prompt = 'Would you like to save this image?' + this_q = Question(self.bot, ctx.channel, prompt, 'yesno', 15, player_embed) + resp = await this_q.ask([ctx.author]) + + if not resp: + await ctx.send('That\'s okay. It wasn\'t a very good picture anyway.') + return + else: + await patch_player(player['id'], vanity_card=url) + await ctx.send(random_conf_word()) + + @commands.command(name='getfc', help='Get last season vanity card') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME) + async def get_vanity_card_command(self, ctx, *, player_name): + if ctx.channel.name == 'season-5-chat': + await ctx.send('Not in season 5 chat, dumbass.') + return + + current = await get_current() + this_team = await get_team_by_owner(current['season'], ctx.author.id) + + if not this_team: + await ctx.send('Hmm...I can\'t find your team. Are you from around here?') + return + + player = await get_one_player( + await fuzzy_player_search(ctx, ctx.channel, self.bot, player_name, self.player_list.keys()) + ) + + old_player = await get_one_player(player["name"], season=current["season"]-1) + await patch_player( + pid=player["id"], + vanity_card=old_player["vanity_card"] + ) + player["vanity_card"] = old_player["vanity_card"] + + player_embed = await get_player_embed(player, current) + await ctx.send(content=None, embed=player_embed) + + @commands.command(name='charts', aliases=['chart'], help='Run `/charts` for all charts') + async def legacy_charts_command(self, ctx): + await ctx.send(f'Run `/charts` for all charts') + + @app_commands.command(name='charts') + async def chart_command(self, interaction: discord.Interaction, chart_name: Literal[ + 'rest', 'sac-bunt', 'squeeze-bunt', 'rob-hr', 'defense-matters', 'block-plate', 'hit-and-run', + 'g1', 'g2', 'g3', 'groundball']): + gb_url = 'https://sombaseball.ddns.net/static/images/season04/ground-ball-chart' + all_charts = { + 'rest': [f'{SBA_IMAGE_URL}/season05/charts/rest.png'], + 'sac-bunt': [ + f'{SBA_IMAGE_URL}/season05/charts/sac-bunt.png', + f'{SBA_IMAGE_URL}/season05/charts/sac-bunt-help.png' + ], + 'squeeze-bunt': [ + f'{SBA_IMAGE_URL}/season05/charts/squeeze-bunt.png', + f'{SBA_IMAGE_URL}/season05/charts/squeeze-bunt-help.png' + ], + 'rob-hr': [f'{SBA_IMAGE_URL}/season05/charts/rob-hr.png'], + 'defense-matters': [f'{SBA_IMAGE_URL}/season05/charts/defense-matters.png'], + 'block-plate': [f'{SBA_IMAGE_URL}/season05/charts/block-plate.png'], + 'hit-and-run': [ + f'{SBA_IMAGE_URL}/season05/charts/hit-and-run.png', + f'{SBA_IMAGE_URL}/season05/charts/hit-and-run-pitcher.png' + ], + 'g1': [f'{gb_url}-g1.png', f'{gb_url}01.png', f'{gb_url}02.png'], + 'g2': [f'{gb_url}-g2.png', f'{gb_url}01.png', f'{gb_url}02.png'], + 'g3': [f'{gb_url}-g3.png', f'{gb_url}01.png', f'{gb_url}02.png'], + 'groundball': [f'{gb_url}01.png', f'{gb_url}02.png'] + } + + first = True + for link in all_charts[chart_name]: + if first: + await interaction.response.send_message(link) + first = False + else: + await interaction.followup.send(link) + + @commands.command(name='pitcherbatting', aliases=['pbat', 'pitbat'], help='Show specific pitcher batting card') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def pitcher_batting_command(self, ctx: commands.Context, card_num: int = 1): + if card_num > 8: + await ctx.send(f'I only have pitcher batting cards 1 through 8. Why don\'t you try again.') + return + if card_num < 1: + await ctx.send(f'I only have pitcher batting cards 1 through 8. Why don\'t you try again.') + return + + team = await get_team_by_owner(SBA_SEASON, ctx.author.id) + embed = get_team_embed(f'Pitcher Batting Card #{card_num}', team=team, thumbnail=False) + embed.set_image(url=f'{SBA_BASE_URL}/static/images/pitbat/card-{card_num}.png') + await ctx.send(content=None, embed=embed) + + @app_commands.command(name='pitupdate', description='Mod: Update pitcher ratings') + @app_commands.checks.has_any_role('Da Commish') + async def pit_update_slash( + self, interaction: discord.Interaction, player_name: str, swar: float = None, injury_games: int = None): + player = await get_one_player( + await fuzzy_player_search(interaction, interaction.channel, self.bot, player_name, self.player_list), + ) + current = await get_current() + + await patch_player( + player['id'], + wara=swar if swar is not None else player['wara'], + injury_rating=f'{player["injury_rating"][:2]}{injury_games}' if injury_games is not None else player["injury_rating"] + ) + new_player = await get_one_player(player['id']) + await interaction.response.send_message(content=None, embed=await get_player_embed(new_player, current)) + + @app_commands.command(name='keepers', description='Mod: Set team keepers') + @app_commands.checks.has_any_role('Da Commish') + async def set_keepers_slash( + self, interaction: discord.Interaction, team_abbrev: str, keep1: str = None, keep2: str = None, + keep3: str = None, keep4: str = None, keep5: str = None, keep6: str = None, keep7: str = None): + team = await get_one_team(team_abbrev) + + keepers = [] + keeper_string = '' + keeper_swar = 0 + + def get_pos_abbrev(position): + if 'B' in position: + return 'IF' + if 'F' in position: + return 'OF' + if 'P' in position: + return 'P' + if position == 'C': + return 'C' + return 'DH' + + for x in [keep1, keep2, keep3, keep4, keep5, keep6, keep7]: + if x: + this_p = await get_one_player( + await fuzzy_player_search(interaction, interaction.channel, self.bot, x, self.player_list) + ) + keepers.append(this_p) + keeper_string += f'{get_pos_abbrev(this_p["pos_1"])} - {this_p["name"]} ({this_p["wara"]:.2f})\n' + keeper_swar += this_p['wara'] + + await interaction.response.send_message(content=f'{team["sname"]} Keepers:\n{keeper_string}') + all_players = db_get('players', api_ver=3, params=[('team_abbrev', team['abbrev'])]) + logging.info(f'all_players: {all_players}') + + fa = await get_one_team('FA') + for y in all_players['players']: + if y not in keepers: + await patch_player(y['id'], team_id=fa['id']) + + await interaction.channel.send( + f'Just yeeted **{len(all_players["players"]) - len(keepers)}** players into the sun!' + ) + + embed = get_team_embed(title=f'{team["lname"]} Keepers', team=team) + embed.add_field(name=f'Keepers', value=keeper_string) + embed.add_field(name='Total sWAR', value=f'{keeper_swar:.2f}') + + await send_to_channel(self.bot, 'sba-network-news', content=None, embed=embed) + + group_publish = app_commands.Group(name='publish', description='Make a scorecard publicly accessible') + + @group_publish.command(name='josef') + @app_commands.describe(sheet_url='The full URL of your publicly shared scorecard') + async def pub_josef_slash(self, interaction: discord.Interaction, sheet_url: str): + sheets = pygsheets.authorize(service_file='storage/major-domo-service-creds.json') + + try: + if 'https://' in sheet_url: + scorecard = sheets.open_by_url(sheet_url).worksheet_by_title('ASCII') + else: + scorecard = sheets.open_by_key(sheet_url).worksheet_by_title('ASCII') + except Exception as e: + await interaction.response.send_message( + 'Frick. I wasn\'t able to publish that card. Is that card ID correct?' + ) + return + + self.scorecards[f'{interaction.channel_id}'] = scorecard + await interaction.response.send_message( + f'Your scorecard has been published. Anyone may now run `/scorebug` in ' + f'this channel to get the scorebug.') + + @app_commands.command(name='scorebug', description='Pull the scorebug for the game in this channel') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME) + async def scorebug_slash(self, interaction: discord.Interaction): + if not self.scorecards: + await interaction.response.send_message( + 'Uhh...I don\'t see any games in this channel. You in the right place?' + ) + return + + this_scorecard = self.scorecards[f'{interaction.channel_id}'] + if not this_scorecard: + this_game = get_one_game(channel_id=interaction.channel_id) + if not this_game: + await interaction.response.send_message( + 'Uhh...I don\'t see any games in this channel. You in the right place?' + ) + return + + embed = self.bot.get_cog('Gameplay').get_game_state_embed(this_game, full_length=False) + await interaction.response.send_message(content=None, embed=embed) + + score_text = this_scorecard.get_value('A1') + await interaction.response.send_message(score_text) + + +async def setup(bot): + await bot.add_cog(Players(bot)) diff --git a/cogs/transactions.py b/cogs/transactions.py new file mode 100644 index 0000000..37a4898 --- /dev/null +++ b/cogs/transactions.py @@ -0,0 +1,1979 @@ +import re +import copy + +from helpers import * +from db_calls import * +from discord.ext import commands, tasks +OFFSEASON_FLAG = False + + +class SBaTransaction: + def __init__(self, channel, current, move_type, this_week=False, first_team=None, team_role=None): + self.players = {} # Example: { : { "player": { Strat Player Dict }, "to": { Strat Team Dict } } } + self.teams = {} # Example: { : { "team": { Strat Team Dict }, "role": } } } + self.picks = {} # Example: { : { "pick": { Strat Pick Dict }, "to": { Strat Team Dict } } } + self.avoid_freeze = True + self.channel = channel + self.gms = [] + self.current = current + self.effective_week = current['week'] + 1 if not this_week else current['week'] + self.move_type = move_type + + if first_team and team_role: + self.add_team(first_team, team_role) + if (first_team and not team_role) or (team_role and not first_team): + logging.error(f'Trade creation failed:\nteam: {first_team}\nteam_role: {team_role}') + raise ValueError('Trade creation failed') + + def add_team(self, new_team, role): + if new_team['id'] not in self.teams.keys(): + self.teams[new_team['id']] = { + 'team': new_team, + 'role': role + } + self.gms.append(new_team['gmid']) + if new_team['gmid2']: + self.gms.append(new_team['gmid2']) + + return True + + async def add_player(self, player, to_team): + if player['id'] not in self.players.keys(): + self.players[player['id']] = { + 'player': player, + 'to': to_team + } + await self.send(f'Got it - throwing {player["name"]} onto a plane.') + else: + await self.send(f'{player["name"]} is already part of this transaction. They\'re heading to ' + f'{self.players[player["id"]]["to"]["sname"]}. Try to keep up.') + + async def add_pick(self, pick, to_team): + if pick['id'] not in self.picks.keys(): + self.picks[pick['id']] = { + 'pick': pick, + 'to': to_team + } + await self.send(f'Okay, {pick["origowner"]["abbrev"]}\'s {pick["round"]} is on the table.') + else: + await self.send(f'{pick["origowner"]["abbrev"]}\'s {pick["round"]} is already part of the deal. It\'s ' + f'going to {self.picks[pick["id"]]["to"]["abbrev"]}. Please try to keep up.') + + def included_team(self, team): + team_abbrev = team['abbrev'] + for team in self.teams.values(): + if team_abbrev in [team['team']['abbrev'], f'{team["team"]["abbrev"]}IL', f'{team["team"]["abbrev"]}MiL']: + return True + + return False + + async def not_available(self, player, this_week=False): + transactions = await self.get_player_moves(player, this_week) + logging.info(f'Is {player["name"]} not available? - {len(transactions)}') + if len(transactions) > 0: + return True + return False + + def get_gms(self, bot, team=None): + if team is None: + return [bot.get_user(x) for x in self.gms] + else: + these_gms = [bot.get_user(self.teams[team]['team']['gmid'])] + if self.teams[team]['team']['gmid2']: + these_gms.append(bot.get_user(self.teams[team]['team']['gmid2'])) + return these_gms + + async def send(self, content=None, embed=None): + return await self.channel.send(content=content, embed=embed) + + async def show_moves(self, here=True): + embed = get_team_embed(f'Week {self.effective_week} Transaction', team=[*self.teams.values()][0]['team']) + teams = [self.teams[x]['team']['sname'] for x in self.teams] + embed.description = f'{", ".join(teams)}' + + # Get player string + player_string = '' + for x in self.players: + player_string += f'**{self.players[x]["player"]["name"]}** ({self.players[x]["player"]["wara"]}) from ' \ + f'{self.players[x]["player"]["team"]["abbrev"]} to {self.players[x]["to"]["abbrev"]}\n' + + if len(player_string) == 0: + player_string = 'None...yet' + + embed.add_field(name='Player Moves', value=player_string, inline=False) + + # Get draft pick string + pick_string = '' + for x in self.picks: + pick_string += f'**{self.picks[x]["pick"]["origowner"]["abbrev"]} {self.picks[x]["pick"]["round"]}** to ' \ + f'{self.picks[x]["to"]["abbrev"]}\n' + + if len(pick_string) > 0: + embed.add_field(name='Pick Trades', value=pick_string, inline=False) + + if here: + await self.send(content=None, embed=embed) + else: + return embed + + async def timed_delete(self): + await self.channel.send('I will delete this channel in 5 minutes.') + await asyncio.sleep(300) + await self.channel.delete() + + async def get_player_moves(self, player, this_week): + if this_week: + return await get_transactions( + self.current['season'], + week_start=self.current['week'], + player_id=player['id'] + ) + else: + return await get_transactions( + self.current['season'], + week_start=self.effective_week, + player_id=player['id'] + ) + + async def check_major_league_errors(self, team): + wara = 0 + mil_wara = 0 + this_team = self.teams[team]['team'] + team_roster = await get_players(self.current['season'], this_team['abbrev']) + mil_roster = await get_players(self.current['season'], f'{this_team["abbrev"]}MiL') + + for player in team_roster: + wara += team_roster[player]['wara'] + for player in mil_roster: + mil_wara += mil_roster[player]['wara'] + + if self.effective_week > self.current['week']: + set_moves = await get_transactions( + self.current['season'], team_abbrev=this_team['abbrev'], week_start=self.effective_week, + week_end=self.effective_week + ) + freeze_moves = await get_transactions( + self.current['season'], team_abbrev=this_team['abbrev'], week_start=self.effective_week, + week_end=self.effective_week, frozen=True + ) + moves = {**set_moves, **freeze_moves} + + for x in moves: + # If player is joining this team, add to roster and add WARa + if moves[x]['newteam'] == this_team: + team_roster[moves[x]['player']['name']] = moves[x]['player'] + wara += moves[x]['player']['wara'] + # If player is joining MiL team, add to roster and add WARa + elif moves[x]['newteam']['id'] == this_team['id'] + 2: + mil_roster[moves[x]['player']['name']] = moves[x]['player'] + mil_wara += moves[x]['player']['wara'] + + # If player is leaving this team, remove from roster and subtract WARa + if moves[x]['oldteam'] == this_team: + # del team_roster[moves[x]['player']['name']] + team_roster.pop(moves[x]['player']['name'], None) + wara -= moves[x]['player']['wara'] + # If player is leaving MiL team, remove from roster and subtract WARa + elif moves[x]['oldteam']['id'] == this_team['id'] + 2: + # del team_roster[moves[x]['player']['name']] + mil_roster.pop(moves[x]['player']['name'], None) + mil_wara -= moves[x]['player']['wara'] + + for x in self.players: + # If player is joining this team, add to roster and add WARa + if self.players[x]['to'] == this_team: + team_roster[self.players[x]['player']['name']] = self.players[x]['player'] + wara += self.players[x]['player']['wara'] + # If player is joining MiL team, add to roster and add WARa + elif self.players[x]['to']['abbrev'] == f'{this_team["abbrev"]}MiL': + mil_roster[self.players[x]['player']['name']] = self.players[x]['player'] + mil_wara += self.players[x]['player']['wara'] + # If player is joining IL team, remove from roster and cut WARa + elif self.players[x]['to']['abbrev'] == f'{this_team["abbrev"]}IL': + wara -= self.players[x]['player']['wara'] + + # If player is leaving this team next week, remove from roster and subtract WARa + if self.players[x]['player']['team'] == this_team: + team_roster.pop(self.players[x]['player']['name'], None) + # 06-13: COMMENTED OUT TO RESOLVE MID-WEEK IL REPLACEMENT BEING SENT BACK DOWN + # if self.effective_week != self.current['week']: + wara -= self.players[x]['player']['wara'] + + # If player is leaving MiL team next week, remove from roster and subtract WARa + if self.players[x]['player']['team']['abbrev'] == f'{this_team["abbrev"]}MiL': + mil_roster.pop(self.players[x]['player']['name'], None) + if self.effective_week != self.current['week']: + mil_wara -= self.players[x]['player']['wara'] + + return {'roster': team_roster, 'wara': wara, 'mil_roster': mil_roster, 'mil_wara': mil_wara} + + # async def check_minor_league_errors(self, ml_team_id): + # wara = 0 + # this_team = await get_one_team(ml_team_id + 2) + # team_roster = await get_players(self.current['season'], this_team['abbrev']) + # + # for player in team_roster: + # wara += team_roster[player]['wara'] + # + # if self.effective_week > self.current['week']: + # set_moves = await get_transactions( + # self.current['season'], team_abbrev=this_team['abbrev'], week_start=self.effective_week, + # week_end=self.effective_week + # ) + # freeze_moves = await get_transactions( + # self.current['season'], team_abbrev=this_team['abbrev'], week_start=self.effective_week, + # week_end=self.effective_week, frozen=True + # ) + # moves = {**set_moves, **freeze_moves} + # + # for x in moves: + # # If player is joining this team, add to roster and add WARa + # if moves[x]['newteam'] == this_team: + # team_roster[moves[x]['player']['name']] = moves[x]['player'] + # wara += moves[x]['player']['wara'] + # # If player is leaving this team, remove from roster and subtract WARa + # else: + # # del team_roster[moves[x]['player']['name']] + # team_roster.pop(moves[x]['player']['name'], None) + # wara -= moves[x]['player']['wara'] + # + # for x in self.players: + # # If player is joining this team, add to roster and add WARa + # if self.players[x]['to'] == this_team: + # team_roster[self.players[x]['player']['name']] = self.players[x]['player'] + # wara += self.players[x]['player']['wara'] + # # If player is leaving this team next week, remove from roster and subtract WARa + # elif self.players[x]['player']['team'] == this_team: + # team_roster.pop(self.players[x]['player']['name'], None) + # if self.effective_week != self.current['week']: + # wara -= self.players[x]['player']['wara'] + # + # return {'roster': team_roster, 'wara': wara} + + async def send_transaction(self): + team_id = list(self.teams.keys())[0] + moveid = f'Week-{self.effective_week:0>2}-{datetime.datetime.now().strftime("%d-%H:%M:%S")}' + moves = [] + + logging.warning(f'move_id: {moveid} / move_type: {self.move_type} / avoid_freeze: {self.avoid_freeze} / ' + f'week: {self.current["week"]}') + if self.current['freeze'] and not self.avoid_freeze: + frozen = True + else: + frozen = False + + for x in self.players: + moves.append({ + 'week': self.effective_week, + 'player_id': self.players[x]['player']['id'], + 'oldteam_id': self.players[x]['player']['team']['id'], + 'newteam_id': self.players[x]['to']['id'], + 'season': self.current['season'], + 'moveid': moveid, + 'frozen': frozen + }) + + await post_transactions(moves) + + for x in self.picks: + await patch_draftpick(self.picks[x]['pick']['id'], owner_id=self.picks[x]['to']['id']) + + return moveid + + def __str__(self): + trans_string = 'Players:\n' + for x in self.players: + trans_string += f'{self.players[x]}\n' + trans_string += '\nTeams:\n' + for x in self.teams: + trans_string += f'{self.teams[x]}\n' + trans_string += f'\nChannel: {self.channel}\n\nGMs:\n' + for x in self.gms: + trans_string += f'{x}\n' + + return trans_string + + +class Transactions(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.trade_season = False + + self.weekly_loop.start() + + async def cog_command_error(self, ctx, error): + await ctx.send(f'{error}') + + @tasks.loop(minutes=1) + async def weekly_loop(self): + try: + guild = self.bot.get_guild(int(os.environ.get('GUILD_ID'))) + if not guild: + logging.error(f'Could not pull guild; exiting loop') + return + except Exception as e: + logging.error(f'Could not run weekly_loop: {e}') + return + + current = await get_current() + now = datetime.datetime.now() + logging.info(f'Datetime: {now} / weekday: {now.weekday()}') + + # Begin Freeze + # if now.weekday() == 0 and now.hour == 5 and not current['freeze']: # Spring/Summer + if now.weekday() == 0 and now.hour == 6 and not current['freeze']: # Fall/Winter + current['week'] += 1 + if OFFSEASON_FLAG: + if not self.trade_season: + await patch_current(week=current['week']) + await self.run_transactions(current) + stars = f'{"":*<26}' + freeze_message = f'```\n' \ + f'{stars}\n' \ + f' IT\'S TRADE SZN BITCHES\n' \ + f'{stars}\n```' + logging.info(f'Freeze string:\n\n{freeze_message}') + await send_to_channel(self.bot, 'sba-network-news', freeze_message) + self.trade_season = True + else: + await patch_current(week=current['week'], freeze=True) + await self.run_transactions(current) + await self.update_roster_sheet(current['season']) + + logging.info(f'Building freeze string') + week_num = f'Week {current["week"]}' + stars = f'{"":*<32}' + freeze_message = f'```\n' \ + f'{stars}\n'\ + f'{week_num: >9} Freeze Period Begins\n' \ + f'{stars}\n```' + logging.info(f'Freeze string:\n\n{freeze_message}') + await send_to_channel(self.bot, 'transaction-log', freeze_message) + + # End Freeze + # elif now.weekday() == 5 and now.hour == 5 and current['freeze']: # Spring/Summer + elif now.weekday() == 5 and now.hour == 6 and current['freeze']: # Fall/Winter + if not OFFSEASON_FLAG: + await patch_current(freeze=False) + + week_num = f'Week {current["week"]}' + stars = f'{"":*<30}' + freeze_message = f'```\n' \ + f'{stars}\n'\ + f'{week_num: >9} Freeze Period Ends\n' \ + f'{stars}\n```' + await self.process_freeze_moves(current) + await send_to_channel(self.bot, 'transaction-log', freeze_message) + self.trade_season = False + + async def run_transactions(self, current): + all_moves = await get_transactions(current['season'], week_start=current['week'], week_end=current['week']) + if len(all_moves) == 0: + return + + for move in [*all_moves.values()]: + try: + if (move['newteam']['abbrev'][-3:] == 'MiL' and move['oldteam']['abbrev'] == 'FA') or \ + move['newteam']['abbrev'][-2:] == 'IL' or move['newteam']['abbrev'].lower() == 'fa': + dem_week = current['week'] + else: + dem_week = current['week'] + 1 + + await patch_player( + move['player']['id'], + team_id=move['newteam']['id'], + demotion_week=dem_week + ) + except Exception as e: + await send_to_channel(self.bot, 'commissioners-office', f'Error running move:\n{move["moveid"]}') + + async def process_freeze_moves(self, current): + all_moves = await get_transactions( + season=current['season'], + week_start=current['week'], + week_end=current['week'] + 1, + frozen=True + ) + if len(all_moves) == 0: + logging.warning(f'No transactions to process for the freeze in week {current["week"]}') + else: + logging.info(f'freeze / all_moves: {len(all_moves)}') + + # {'player name': [[Player, TeamAdding, moveid], [Player, OtherTeamAdding, moveid]]} + added_players = {} + contested_players = {} + + for move in [*all_moves.values()]: + team_record = await get_team_record(move['newteam'], week_num=current["week"]) + tiebreaker = team_record["pct"] + (random.randint(10000, 99999) * .00000001) + if move["player"]["name"] not in added_players.keys(): + added_players[move["player"]["name"]] = [[move["player"], move["newteam"], tiebreaker, move["moveid"]]] + else: + added_players[move["player"]["name"]].append( + [move["player"], move["newteam"], tiebreaker, move["moveid"]] + ) + logging.info(f'freeze / added_players: {added_players.keys()}') + + # Check added_players for keys (player names) with more than one move in their list + for name in added_players: + if len(added_players[name]) > 1: + contested_players[name] = added_players[name] + logging.info(f'freeze / contested_players: {contested_players.keys()}') + + # Determine winner for contested players, mark moveid cancelled for loser + def tiebreaker(val): + logging.info(f'tiebreaker: {val}') + return val[2] + + for guy in contested_players: + contested_players[guy].sort(key=tiebreaker) + first = True + logging.info(f'Contested Player: {contested_players[guy]}\n\n') + for x in contested_players[guy]: + logging.info(f'First: {first} / x: {x}\n\n') + if not first: + await patch_transaction(move_id=x[3], cancelled=True, frozen=False) + else: + first = False + + # Post transactions that are not cancelled + final_moves = await get_transactions( + season=current['season'], + week_start=current["week"], + week_end=current["week"] + 1, + frozen=True + ) + + these_ids = [] + for x in final_moves: + if final_moves[x]["moveid"] not in these_ids: + these_ids.append(final_moves[x]["moveid"]) + + # TODO: not sure I like this method of running moves + for move_id in these_ids: + await patch_transaction(move_id, frozen=False) + await self.post_move_to_transaction_log(move_id) + await self.send_move_to_sheets(move_id) + + # # In case there were SIL moves that go this week, run transactions + # await self.run_transactions(current) + + async def send_move_to_sheets(self, move_id): + return + current = await get_current() + sheets = pygsheets.authorize(service_file='storage/major-domo-service-creds.json') + trans_tab = sheets.open_by_key(SBA_ROSTER_KEY).worksheet_by_title('Transactions') + all_vals = [] + all_moves = await get_transactions( + season=current['season'], + move_id=move_id + ) + + counter = 0 + for move in [*all_moves.values()]: + all_vals.append([ + move['player']['name'], + move['oldteam']['sname'], + move['newteam']['sname'], + move['week'], + current['transcount'] + counter + ]) + counter += 1 + + try: + trans_tab.update_values( + crange=f'A{current["transcount"] + 3}', + values=all_vals + ) + await patch_current(transcount=current['transcount'] + counter) + except Exception as e: + await send_to_channel(self.bot, 'commissioners-office', f'Failed sending move {move_id} to sheets') + + async def post_move_to_transaction_log(self, move_id): + current = await get_current() + all_moves = await get_transactions( + season=current['season'], + move_id=move_id + ) + + this_team = None + week_num = None + move_string = '' + + for move in [*all_moves.values()]: + if not this_team: + if move['oldteam']['abbrev'] != 'FA' and 'IL' not in move['oldteam']['abbrev']: + this_team = await get_one_team(move['oldteam']['id']) + elif move['newteam']['abbrev'] != 'FA' and 'IL' not in move['newteam']['abbrev']: + this_team = await get_one_team(move['newteam']['id']) + if not week_num: + week_num = move['week'] + move_string += f'**{move["player"]["name"]}** ({move["player"]["wara"]}) from ' \ + f'{move["oldteam"]["abbrev"]} to {move["newteam"]["abbrev"]}\n' + + embed = get_team_embed(f'Week {week_num} Transaction', this_team) + embed.description = this_team['sname'] + embed.add_field(name='Player Moves', value=move_string) + + await send_to_channel(self.bot, 'transaction-log', embed=embed) + + async def send_stats_to_sheets(self, channel='commissioners-office', which='all'): + current = await get_current() + b_stats = None + p_stats = None + + if which == 'all' or which == 'batting': + await send_to_channel(self.bot, channel, 'Collecting batting stats...') + b_stats = await get_battingstat(current['season'], timeout=90) + + if which == 'all' or which == 'pitching': + await send_to_channel(self.bot, channel, 'Collecting pitching stats...') + p_stats = await get_pitchingstat(current['season'], timeout=90) + + sheets = pygsheets.authorize(service_file='storage/major-domo-service-creds.json') + if b_stats: + await send_to_channel(self.bot, channel, f'Preparing batting stats ({len(b_stats)} lines found)...') + batting_stats = [] + + for x in [*b_stats.values()]: + batting_stats.append([ + x['player']['name'], x['team']['abbrev'], x['pos'], x['pa'], x['ab'], x['run'], x['hit'], x['rbi'], + x['double'], x['triple'], x['hr'], x['bb'], x['so'], x['hbp'], x['sac'], x['ibb'], x['gidp'], + x['sb'], x['cs'], x['xch'], x['xhit'], x['error'], x['pb'], x['sbc'], x['csc'], x['week'], + x['game'], f'{x["week"]}.{x["game"]}' + ]) + await patch_current(bstatcount=len(batting_stats)) + await send_to_channel(self.bot, channel, f'Sending {len(batting_stats)} batting lines...') + sheets.open_by_key(SBA_STATS_KEY).worksheet_by_title('Batting Data').update_values( + crange='A2', + values=batting_stats + ) + await send_to_channel(self.bot, channel, f'Batting stats have been sent') + elif which == 'all' or which == 'batting': + await send_to_channel(self.bot, channel, 'No batting stats found') + + if p_stats: + await send_to_channel(self.bot, channel, f'Preparing pitching stats ({len(p_stats)} lines found)...') + pitching_stats = [] + + for x in [*p_stats.values()]: + pitching_stats.append([ + x['player']['name'], x['team']['abbrev'], x['ip'], x['hit'], x['run'], x['erun'], x['so'], x['bb'], + x['hbp'], x['wp'], x['balk'], x['hr'], 1 if x['gs'] else 0, 1 if x['win'] else 0, + 1 if x['loss'] else 0, 1 if x['hold'] else 0, 1 if x['sv'] else 0, 1 if x['bsv'] else 0, x['week'], + x['game'], f'{x["week"]}.{x["game"]}' + ]) + await patch_current(pstatcount=len(pitching_stats)) + await send_to_channel(self.bot, channel, f'Sending {len(pitching_stats)} pitching lines...') + sheets.open_by_key(SBA_STATS_KEY).worksheet_by_title('Pitching Data').update_values( + crange='A2', + values=pitching_stats + ) + await send_to_channel(self.bot, channel, f'Pitching stats have been sent') + elif which == 'all' or which == 'pitching': + await send_to_channel(self.bot, channel, 'No pitching stats found') + + async def update_roster_sheet(self, season): + logging.info(f'calling the db') + # csv_data = db_get('players', api_ver=2, params=[('season', 6), ('csv', True)], as_csv=True) + # csv = DataFrame(csv_data).to_csv(header=False, index=False) + # csv = pandas.read_csv(csv_data) + + ap = await get_players(season) + player_data = [ + ['name', 'sWAR', 'image', 'vanity_card', 'team_abbrev', 'inj_rat', 'pos_1', 'pos_2', 'pos_3', 'pos_4', + 'pos_5', 'pos_6', 'pos_7', 'pos_8', 'last_game', 'last_game2', 'il_return', 'dem_week', 'strat_code', + 'bbref_id'] + ] + for x in ap: + player_data.append([ + ap[x]['name'], ap[x]['wara'], ap[x]['image'], ap[x]['vanity_card'], ap[x]['team']['abbrev'], + ap[x]['injury_rating'], ap[x]['pos_1'], ap[x]['pos_2'], ap[x]['pos_3'], ap[x]['pos_4'], ap[x]['pos_5'], + ap[x]['pos_6'], ap[x]['pos_7'], ap[x]['pos_8'], ap[x]['last_game'], ap[x]['last_game2'], + ap[x]['il_return'], ap[x]['demotion_week'], ap[x]['strat_code'], ap[x]['bbref_id'] + ]) + # logging.info(f'\n\nCSV:\n{player_data}\n') + # auth sheets + logging.info(f'authorizing sheets') + sheets = pygsheets.authorize(service_file='storage/major-domo-service-creds.json', retries=1) + # get sheet + logging.info(f'getting sheet') + master_sheet = sheets.open_by_key(SBA_ROSTER_KEY) + # get worksheet + logging.info(f'getting worksheet') + roster_sheet = master_sheet.worksheet_by_title('API Import') + + logging.info(f'updating values') + roster_sheet.update_values( + crange='A1', + values=player_data + ) + + @staticmethod + def on_team_il(team, player): + player_team_abbrev = player['team']['abbrev'] + sil_abbrev = f'{team["abbrev"]}IL' + mil_abbrev = f'{team["abbrev"]}MiL' + + if player_team_abbrev in [sil_abbrev, mil_abbrev]: + return True + else: + return False + + @commands.command(name='run_transactions') + @commands.is_owner() + async def run_transactions_helper_command(self, ctx): + current = await get_current() + await self.run_transactions(current) + await self.update_roster_sheet(current['season']) + await ctx.send(new_rand_conf_gif()) + + @commands.command(name='process_freeze') + @commands.is_owner() + async def process_freeze_helper_command(self, ctx): + current = await get_current() + await self.process_freeze_moves(current) + await self.update_roster_sheet(current['season']) + await ctx.send(random_conf_gif()) + + @commands.command(name='trade', help='Trade players') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME) + async def trade_command(self, ctx): + current = await get_current() + + if current['week'] > current['trade_deadline']: + await ctx.send(await get_emoji(ctx, 'oof', False)) + await ctx.send(f'The trade deadline is **week {current["trade_deadline"]}**. Since it is currently **week ' + f'{current["week"]}** I am just going to stop you right there.') + return + if current['week'] < -2: + await ctx.send(await get_emoji(ctx, 'oof', False)) + await ctx.send(f'Patience, grasshopper. Trades open soon.') + return + + team = await get_team_by_owner(current['season'], ctx.author.id) + team_role = get_team_role(ctx, team) + player_cog = self.bot.get_cog('Players') + poke_role = get_role(ctx, 'Pokétwo') + + # Create trade channel + overwrites = {ctx.guild.default_role: discord.PermissionOverwrite(read_messages=False), + team_role: discord.PermissionOverwrite(read_messages=True), + ctx.guild.me: discord.PermissionOverwrite(read_messages=True), + poke_role: discord.PermissionOverwrite(read_messages=True, send_messages=False)} + + try: + t_channel = await ctx.guild.create_text_channel( + f'{team["abbrev"]}-trade', + overwrites=overwrites, + category=discord.utils.get(ctx.guild.categories, name=f'Transactions') + ) + # t_channel = await create_channel( + # ctx, + # channel_name=f'{team["abbrev"]}-trade', + # category_name=f'Transactions', + # everyone_read=False, + # read_send_roles=team_role + # ) + except Exception as e: + await ctx.send(f'{e}\n\n' + f'Discord is having issues creating private channels right now. Please try again later.') + return + + # Create trade and post to channel + trade = SBaTransaction(t_channel, current, 'trade', first_team=team, team_role=team_role) + await trade.send(f'Let\'s start here, {team_role.mention}.') + await ctx.send(f'Take my hand... {trade.channel.mention}') + intro = await trade.send( + 'First, we will add each team involved in the trade. Then I will have you enter each of the players ' + 'involved with the trade and where they are going.\n\nOnce that is done, each of the teams involved ' + 'will need to confirm the trade.' + ) + await intro.pin() + + # Add teams loop + while True: + prompt = 'Are you adding a team to this deal? (Yes/No)' + this_q = Question(self.bot, trade.channel, prompt, 'yesno', 30) + resp = await this_q.ask(trade.get_gms(self.bot)) + + if resp is None: + await trade.send('RIP this move. Maybe next time.') + await trade.timed_delete() + return + elif not resp: + break + else: + this_q.prompt = 'Please enter the team\'s abbreviation.' + this_q.qtype = 'text' + resp = await this_q.ask(trade.get_gms(self.bot)) + + if not resp: + await trade.send('RIP this move. Maybe next time.') + await trade.timed_delete() + return + else: + try: + next_team = await get_one_team(resp) + except ValueError: + await trade.send('Who the fuck even is that? Try again.') + else: + next_team_role = get_team_role(ctx, next_team) + overwrites[next_team_role] = discord.PermissionOverwrite(read_messages=True) + await trade.channel.edit(overwrites=overwrites) + trade.add_team(next_team, next_team_role) + await trade.send(f'Welcome to the trade, {next_team_role.mention}!') + + # Get player trades + while True: + prompt = f'Are you trading a player between teams?' + this_q = Question(self.bot, trade.channel, prompt, 'yesno', 300) + resp = await this_q.ask(trade.get_gms(self.bot)) + + if resp is None: + await trade.send('RIP this move. Maybe next time.') + await trade.timed_delete() + return + elif not resp: + break + else: + this_q.prompt = 'Which player is being traded?' + this_q.qtype = 'text' + resp = await this_q.ask(trade.get_gms(self.bot)) + + if not resp: + pass + else: + try: + player = await get_one_player( + await fuzzy_player_search(ctx, trade.channel, self.bot, resp, player_cog.player_list.keys()) + ) + except ValueError: + await trade.send(f'{await get_emoji(ctx, "squint")}') + await trade.send('Who even is that? Try again.') + else: + # Check that player is on one of the teams + # Check that the player hasn't been dropped (IL is okay) + if not trade.included_team(player['team']): + await trade.send(f'You know that {player["name"]} is on {player["team"]["abbrev"]}, right? ' + f'They\'re not part of this trade so...nah.') + elif await trade.not_available(player): + await trade.send(f'Ope. {player["name"]} is already one the move next week.') + else: + this_q.prompt = 'Where are they going? Please enter the destination team\'s abbreviation.' + resp = await this_q.ask(trade.get_gms(self.bot)) + + if resp is None: + await trade.send('RIP this move. Maybe next time.') + await trade.timed_delete() + else: + try: + dest_team = await get_one_team(resp) + except ValueError: + await trade.send(f'{await get_emoji(ctx, "facepalm")} They aren\'t even part of ' + f'this trade. Come on.') + else: + if player['team'] == dest_team: + await trade.send(f'{await get_emoji(ctx, "lolwhat")} {player["name"]} ' + f'is already on {dest_team["abbrev"]}.') + elif not trade.included_team(dest_team): + await trade.send(f'{await get_emoji(ctx, "lolwhat")} {dest_team["abbrev"]} ' + f'isn\'t even part of this trade.') + else: + await trade.add_player(player, dest_team) + + await trade.show_moves() + + # Get pick trades + # while True and current['pick_trade_end'] >= current['week'] >= current['pick_trade_start']: + # prompt = f'Are you trading any draft picks?' + # this_q = Question(self.bot, trade.channel, prompt, 'yesno', 300) + # resp = await this_q.ask(trade.get_gms(self.bot)) + # effective_season = current['season'] if OFFSEASON_FLAG else current['season'] + 1 + # team_season = current['season'] + # + # if resp is None: + # await trade.send('RIP this move. Maybe next time.') + # await trade.timed_delete() + # return + # elif not resp: + # break + # else: + # # Get first pick + # this_q.prompt = 'Enter the pick\'s original owner and round number like this: TIT 17' + # this_q.qtype = 'text' + # resp = await this_q.ask(trade.get_gms(self.bot)) + # + # if not resp: + # await trade.send('RIP this move. Maybe next time.') + # await trade.timed_delete() + # return + # else: + # team_abbrev = resp.split(' ')[0] + # round_num = resp.split(' ')[1] + # try: + # first_pick = await get_one_draftpick_search( + # effective_season, + # orig_owner_abbrev=team_abbrev, + # round_num=round_num, + # team_season=team_season + # ) + # except Exception as e: + # await trade.send(f'Uh oh, I couldn\'t find **{team_abbrev} {round_num}**. Check the team and ' + # f'round number, please.') + # else: + # this_q.prompt = 'Now enter the return pick\'s original owner and round number like this: TIT 17' + # resp = await this_q.ask(trade.get_gms(self.bot)) + # + # if not resp: + # await trade.send('RIP this move. Maybe next time.') + # await trade.timed_delete() + # return + # else: + # team_abbrev = resp.split(' ')[0] + # round_num = resp.split(' ')[1] + # try: + # second_pick = await get_one_draftpick_search( + # effective_season, + # orig_owner_abbrev=team_abbrev, + # round_num=round_num, + # team_season=team_season + # ) + # except Exception as e: + # await trade.send( + # f'Uh oh, I couldn\'t find **{team_abbrev} {round_num}**. Check the team and ' + # f'round number, please.') + # else: + # team_getting_first_pick = second_pick['owner'] + # team_getting_second_pick = first_pick['owner'] + # + # if not trade.included_team(first_pick['owner']): + # await trade.send( + # f'Imma let you finish, but **{first_pick["owner"]["abbrev"]}** currently holds ' + # f'{first_pick["origowner"]["abbrev"]} {first_pick["round"]} and are not part ' + # f'of this deal so let\'s try this again.' + # ) + # else: + # if not trade.included_team(second_pick['owner']): + # await trade.send( + # f'Imma let you finish, but **{second_pick["owner"]["abbrev"]}** currently ' + # f'holds {second_pick["origowner"]["abbrev"]} {second_pick["round"]} and ' + # f'are not part of this deal so let\'s try this again.' + # ) + # else: + # await trade.add_pick(first_pick, team_getting_first_pick) + # await trade.add_pick(second_pick, team_getting_second_pick) + # + # await trade.show_moves() + + # FA drops per team + + if current['week'] > 0: + for team in trade.teams: + while True: + this_q.prompt = f'{trade.teams[team]["role"].mention}\nAre you making an FA drop?' + this_q.qtype = 'yesno' + resp = await this_q.ask(trade.get_gms(self.bot, team)) + + if resp is None: + await trade.send('RIP this move. Maybe next time.') + await trade.timed_delete() + return + elif not resp: + break + else: + this_q.prompt = 'Who are you dropping?' + this_q.qtype = 'text' + resp = await this_q.ask(trade.get_gms(self.bot, team)) + + if resp is None: + await trade.send('RIP this move. Maybe next time.') + await trade.timed_delete() + return + else: + try: + player = await get_one_player( + await fuzzy_player_search(ctx, trade.channel, self.bot, resp, + player_cog.player_list.keys()) + ) + except ValueError: + await trade.send(f'{await get_emoji(ctx, "squint")}') + await trade.send('Who even is that? Try again.') + else: + if not trade.included_team(player['team']): + await t_channel.send(f'It looks like {player.name} is on {player.team.abbrev} ' + f'so I can\'t let you do that.') + # elif player['demotion_week'] > current['week']: + # await trade.send(f'Oof. {player["name"]} cannot be dropped until week ' + # f'{player["demotion_week"]}.') + else: + dest_team = await get_one_team('FA') + await trade.add_player(player, dest_team) + + await trade.show_moves() + + # Check for empty move + if len(trade.players) + len(trade.picks) == 0: + await trade.send(f'This has been fun. Come again and maybe do something next time.') + await trade.timed_delete() + return + + # Check legality for all teams + errors = [] + for team in trade.teams: + data = await trade.check_major_league_errors(team) + logging.warning(f'Done checking data - checking WARa now ({data["wara"]}') + + if data['wara'] > 38.001: + errors.append(f'- {trade.teams[team]["team"]["abbrev"]} would have {data["wara"]:.2f} WARa') + + logging.warning(f'Now checking roster {len(data["roster"])}') + if len(data['roster']) > 26: + errors.append(f'- {trade.teams[team]["team"]["abbrev"]} would have {len(data["roster"])} players') + + logging.warning(f'Any errors? {errors}') + if data['wara'] > 38.001 or len(data['roster']) > 26: + roster_string = '' + for x in data['roster']: + roster_string += f'{data["roster"][x]["wara"]: >5} - {data["roster"][x]["name"]}\n' + errors.append(f'- This is the roster I have for {trade.teams[team]["team"]["abbrev"]}:\n' + f'```\n{roster_string}```') + + if len(errors) > 0: + error_message = '\n'.join(errors) + await trade.send(f'Yikes. I\'m gonna put the kibosh on this trade. Below is why:\n\n{error_message}') + await trade.timed_delete() + return + + # Ask for each team's explict confirmation + for team in trade.teams: + this_q.prompt = f'{trade.teams[team]["role"].mention}\nDo you accept this trade?' + this_q.qtype = 'yesno' + resp = await this_q.ask(trade.get_gms(self.bot, team)) + + if not resp: + await trade.send('RIP this move. Maybe next time.') + await trade.timed_delete() + return + else: + await trade.show_moves() + + # Run moves + trans_id = await trade.send_transaction() + + await send_to_channel(self.bot, 'transaction-log', embed=await trade.show_moves(here=False)) + + await trade.send(f'All done! Your transaction id is: {trans_id}') + try: + choas = get_role(ctx, 'CHOAS ALERT') + await send_to_channel(self.bot, f'season-{current["season"]}-chat', f'{choas.mention}') + except Exception as e: + logging.error(f'Couldn\'t ping chaos for a trade') + await trade.timed_delete() + + @commands.command(name='picktrade', help='Trade draft picks', hidden=True) + @commands.is_owner() + async def pick_trade_command(self, ctx): + current = await get_current() + + if current['week'] < -2: + await ctx.send(await get_emoji(ctx, 'oof', False)) + await ctx.send(f'Patience, grasshopper. Trades open up Monday.') + return + if not OFFSEASON_FLAG: + await ctx.send(await get_emoji(ctx, 'oof', False)) + await ctx.send(f'You\'ll have to wait, hoss. No pick trades until the offseason.') + return + + team = await get_team_by_owner(current['season'], ctx.author.id) + team_role = get_team_role(ctx, team) + player_cog = self.bot.get_cog('Players') + + # Create trade channel + overwrites = {ctx.guild.default_role: discord.PermissionOverwrite(read_messages=False), + team_role: discord.PermissionOverwrite(read_messages=True), + ctx.guild.me: discord.PermissionOverwrite(read_messages=True)} + + try: + # t_channel = await ctx.guild.create_text_channel( + # f'{team["abbrev"]}-trade', + # overwrites=overwrites, + # category=discord.utils.get(ctx.guild.categories, name=f'Transactions') + # ) + t_channel = await create_channel( + ctx, + channel_name=f'{team["abbrev"]}-trade', + category_name=f'Transactions', + everyone_read=False, + read_send_roles=team_role + ) + except Exception as e: + await ctx.send(f'{e}\n\n' + f'Discord is having issues creating private channels right now. Please try again later.') + return + + # Create trade and post to channel + trade = SBaTransaction(t_channel, current, 'trade', first_team=team, team_role=team_role) + await trade.send(f'Let\'s start here, {team_role.mention}.') + await ctx.send(f'Take my hand... {trade.channel.mention}') + intro = await trade.send( + 'Alrighty, let\'s collect the pick trades. Once they are all entered, each of the teams involved ' + 'will need to confirm the trade.' + ) + await intro.pin() + + # Add teams loop + while True: + prompt = 'Are you adding a team to this deal? (Yes/No)' + this_q = Question(self.bot, trade.channel, prompt, 'yesno', 30) + resp = await this_q.ask(trade.get_gms(self.bot)) + + if resp is None: + await trade.send('RIP this move. Maybe next time.') + await trade.timed_delete() + return + elif not resp: + break + else: + this_q.prompt = 'Please enter the team\'s abbreviation.' + this_q.qtype = 'text' + resp = await this_q.ask(trade.get_gms(self.bot)) + + if not resp: + await trade.send('RIP this move. Maybe next time.') + await trade.timed_delete() + return + else: + try: + next_team = await get_one_team(resp) + except ValueError: + await trade.send('Who the fuck even is that? Try again.') + else: + next_team_role = get_team_role(ctx, next_team) + overwrites[next_team_role] = discord.PermissionOverwrite(read_messages=True) + await trade.channel.edit(overwrites=overwrites) + trade.add_team(next_team, next_team_role) + await trade.send(f'Welcome to the trade, {next_team_role.mention}!') + + # Get pick trades + while True: + prompt = f'Are you trading any draft picks?' + this_q = Question(self.bot, trade.channel, prompt, 'yesno', 300) + resp = await this_q.ask(trade.get_gms(self.bot)) + effective_season = current['season'] if OFFSEASON_FLAG else current['season'] + 1 + team_season = current['season'] + + if resp is None: + await trade.send('RIP this move. Maybe next time.') + await trade.timed_delete() + return + elif not resp: + break + else: + # Get first pick + this_q.prompt = 'Enter the overall pick number being traded.' + this_q.qtype = 'int' + resp = await this_q.ask(trade.get_gms(self.bot)) + + if not resp: + await trade.send('RIP this move. Maybe next time.') + await trade.timed_delete() + return + else: + try: + first_pick = await get_one_draftpick_byoverall( + effective_season, + overall=resp + ) + except Exception as e: + await trade.send(f'Frick, I couldn\'t find pick #{resp}.') + else: + this_q.prompt = 'Now enter the return pick\'s overall pick number.' + resp = await this_q.ask(trade.get_gms(self.bot)) + + if not resp: + await trade.send('RIP this move. Maybe next time.') + await trade.timed_delete() + return + else: + try: + second_pick = await get_one_draftpick_byoverall( + effective_season, + overall=resp + ) + except Exception as e: + await trade.send(f'Frick, I couldn\'t find pick #{resp}.') + else: + team_getting_first_pick = second_pick['owner'] + team_getting_second_pick = first_pick['owner'] + + if not trade.included_team(first_pick['owner']): + await trade.send( + f'Ope. **{first_pick["owner"]["abbrev"]}** currently holds ' + f'{first_pick["origowner"]["abbrev"]} {first_pick["round"]} and are not part ' + f'of this deal so let\'s try this again.' + ) + else: + if not trade.included_team(second_pick['owner']): + await trade.send( + f'Imma let you finish, but **{second_pick["owner"]["abbrev"]}** currently ' + f'holds {second_pick["origowner"]["abbrev"]} {second_pick["round"]} and ' + f'are not part of this deal so let\'s try this again.' + ) + else: + await trade.add_pick(first_pick, team_getting_first_pick) + await trade.add_pick(second_pick, team_getting_second_pick) + + await trade.show_moves() + + # Check for empty move + if len(trade.players) + len(trade.picks) == 0: + await trade.send(f'This has been fun. Come again and maybe do something next time.') + await trade.timed_delete() + return + + # Check legality for all teams + errors = [] + for team in trade.teams: + data = await trade.check_major_league_errors(team) + logging.warning(f'Done checking data - checking WARa now ({data["wara"]}') + + if data['wara'] > 38.001: + errors.append(f'- {trade.teams[team]["team"]["abbrev"]} would have {data["wara"]:.2f} WARa') + + logging.warning(f'Now checking roster {len(data["roster"])}') + if len(data['roster']) > 26: + errors.append(f'- {trade.teams[team]["team"]["abbrev"]} would have {len(data["roster"])} players') + + logging.warning(f'Any errors? {errors}') + if data['wara'] > 38.001 or len(data['roster']) > 26: + roster_string = '' + for x in data['roster']: + roster_string += f'{data["roster"][x]["wara"]: >5} - {data["roster"][x]["name"]}\n' + errors.append(f'- This is the roster I have for {trade.teams[team]["team"]["abbrev"]}:\n' + f'```\n{roster_string}```') + + if len(errors) > 0: + error_message = '\n'.join(errors) + await trade.send(f'Yikes. I\'m gonna put the kibosh on this trade. Below is why:\n\n{error_message}') + await trade.timed_delete() + return + + # Ask for each team's explict confirmation + for team in trade.teams: + this_q.prompt = f'{trade.teams[team]["role"].mention}\nDo you accept this trade?' + this_q.qtype = 'yesno' + resp = await this_q.ask(trade.get_gms(self.bot, team)) + + if not resp: + await trade.send('RIP this move. Maybe next time.') + await trade.timed_delete() + return + else: + await trade.show_moves() + + # Run moves + trans_id = await trade.send_transaction() + + await send_to_channel(self.bot, 'sba-network-news', embed=await trade.show_moves(here=False)) + await self.send_move_to_sheets(trans_id) + + await trade.send(f'All done! Your transaction id is: {trans_id}') + try: + choas = get_role(ctx, 'CHOAS ALERT') + await send_to_channel(self.bot, f'season-{current["season"]}-chat', f'{choas.mention}') + except Exception as e: + logging.error('I was not able to ping CHOAS ALERT') + await trade.timed_delete() + + @commands.command(name='dropadd', aliases=['drop', 'add', 'adddrop', 'longil'], help='FA/MiL moves') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME) + async def drop_add_command(self, ctx): + current = await get_current() + team = await get_team_by_owner(current['season'], ctx.author.id) + team_schedule = await get_schedule( + current['season'], + team_abbrev1=team["abbrev"], + week_start=current['week'] + 1, + week_end=current['week'] + 1, + ) + team_role = get_team_role(ctx, team) + player_cog = self.bot.get_cog('Players') + poke_role = get_role(ctx, 'Pokétwo') + + if len(team_schedule) == 0 and not OFFSEASON_FLAG and current['week'] != 22: + await ctx.send('It looks like your season is over so transactions are locked.') + return + + # Create transaction channel + overwrites = {ctx.guild.default_role: discord.PermissionOverwrite(read_messages=False), + team_role: discord.PermissionOverwrite(read_messages=True), + ctx.guild.me: discord.PermissionOverwrite(read_messages=True), + poke_role: discord.PermissionOverwrite(read_messages=True, send_messages=False)} + + try: + t_channel = await ctx.guild.create_text_channel( + f'{team["abbrev"]}-transaction', + overwrites=overwrites, + category=discord.utils.get(ctx.guild.categories, name=f'Transactions') + ) + except Exception as e: + await ctx.send(f'{e}\n\n' + f'Discord is having issues creating private channels right now. Please try again later.') + return + + dropadd = SBaTransaction(t_channel, current, 'dropadd', first_team=team, team_role=team_role) + + await dropadd.send(f'Let\'s start here, {team_role.mention}') + await ctx.send(f'Take my hand... {dropadd.channel.mention}') + await dropadd.send(f'This transaction is for __next week__. It will go into effect for ' + f'week {current["week"] + 1}.') + + # Get MiL moves + while True and not OFFSEASON_FLAG: + prompt = f'Are you adding someone to the Minor League roster?' + this_q = Question(self.bot, dropadd.channel, prompt, 'yesno', 300) + resp = await this_q.ask(dropadd.get_gms(self.bot)) + + if resp is None: + await dropadd.send('RIP this move. Maybe next time.') + await dropadd.timed_delete() + return + elif not resp: + break + else: + this_q.prompt = 'Who are you sending to the MiL?' + this_q.qtype = 'text' + resp = await this_q.ask(dropadd.get_gms(self.bot)) + + if resp is None: + await dropadd.send('RIP this move. Maybe next time.') + await dropadd.timed_delete() + return + else: + try: + player = await get_one_player( + await fuzzy_player_search(ctx, dropadd.channel, self.bot, resp, + player_cog.player_list.keys()) + ) + except ValueError: + await dropadd.send(f'{await get_emoji(ctx, "squint")}') + await dropadd.send('Who even is that? Try again.') + else: + fa_team = await get_one_team('FA') + dest_team = await get_one_team(f'{team["abbrev"]}MiL') + if not dropadd.included_team(player['team']) and player['team'] != fa_team: + await t_channel.send(f'It looks like {player["name"]} is on {player["team"]["abbrev"]} ' + f'so I can\'t let you do that.') + elif await dropadd.not_available(player): + await dropadd.send(f'Uh oh, looks like {player["name"]} is already on the move next week.') + elif player['demotion_week'] > current['week']: + await dropadd.send(f'Oof. {player["name"]} cannot be dropped until week ' + f'{player["demotion_week"]}.') + else: + if player['team'] == fa_team: + dropadd.avoid_freeze = False + await dropadd.add_player(player, dest_team) + + await dropadd.show_moves() + + # Get Major League Adds + while True and not OFFSEASON_FLAG: + prompt = f'Are you adding any players to the Major League roster?' + this_q = Question(self.bot, dropadd.channel, prompt, 'yesno', 300) + resp = await this_q.ask(dropadd.get_gms(self.bot)) + + if resp is None: + await dropadd.send('RIP this move. Maybe next time.') + await dropadd.timed_delete() + return + elif not resp: + break + else: + this_q.prompt = 'Who are you adding?' + this_q.qtype = 'text' + resp = await this_q.ask(dropadd.get_gms(self.bot)) + + if resp is None: + await dropadd.send('RIP this move. Maybe next time.') + await dropadd.timed_delete() + return + else: + try: + player = await get_one_player( + await fuzzy_player_search(ctx, dropadd.channel, self.bot, resp, + player_cog.player_list.keys()) + ) + except ValueError: + await dropadd.send(f'{await get_emoji(ctx, "squint")}') + await dropadd.send('Who even is that? Try again.') + else: + if OFFSEASON_FLAG: + if not self.on_team_il(team, player): + await t_channel.send(f'It looks like {player["name"]} is on {player["team"]["abbrev"]} ' + f'so I can\'t let you do that.') + else: + await dropadd.add_player(player, team) + else: + fa_team = await get_one_team('FA') + if player['team'] != fa_team and not self.on_team_il(team, player): + await t_channel.send(f'It looks like {player["name"]} is on {player["team"]["abbrev"]} ' + f'so I can\'t let you do that.') + elif await dropadd.not_available(player): + await dropadd.send(f'Uh oh, looks like {player["name"]} is already on the move ' + f'next week.') + elif player['demotion_week'] > current['week']: + await dropadd.send(f'Oof. {player["name"]} cannot be dropped until week ' + f'{player["demotion_week"]}.') + else: + if player['team'] == fa_team: + dropadd.avoid_freeze = False + await dropadd.add_player(player, team) + + await dropadd.show_moves() + + # Get FA Drops + while True: + prompt = f'Are you dropping anyone to FA?' + this_q = Question(self.bot, dropadd.channel, prompt, 'yesno', 300) + resp = await this_q.ask(dropadd.get_gms(self.bot)) + + if resp is None: + await dropadd.send('RIP this move. Maybe next time.') + await dropadd.timed_delete() + return + elif not resp: + break + else: + this_q.prompt = 'Who are you dropping to FA?' + this_q.qtype = 'text' + resp = await this_q.ask(dropadd.get_gms(self.bot)) + + if resp is None: + await dropadd.send('RIP this move. Maybe next time.') + await dropadd.timed_delete() + return + else: + try: + player = await get_one_player( + await fuzzy_player_search(ctx, dropadd.channel, self.bot, resp, + player_cog.player_list.keys()) + ) + except ValueError: + await dropadd.send(f'{await get_emoji(ctx, "squint")}') + await dropadd.send('Who even is that? Try again.') + else: + if not dropadd.included_team(player['team']): + await t_channel.send(f'It looks like {player["name"]} is on {player["team"]["abbrev"]} ' + f'so I can\'t let you do that.') + elif await dropadd.not_available(player): + await dropadd.send(f'Uh oh, looks like {player["name"]} is already on the move next week.') + elif player['demotion_week'] > current['week']: + await dropadd.send(f'Oof. {player["name"]} cannot be dropped until week ' + f'{player["demotion_week"]}.') + else: + dest_team = await get_one_team('FA') + await dropadd.add_player(player, dest_team) + + await dropadd.show_moves() + + # Check for empty move + if len(dropadd.players) == 0: + await dropadd.send(f'This has been fun. Come again and maybe do something next time.') + await dropadd.timed_delete() + return + + # Check legality + errors = [] + for team in dropadd.teams: + data = await dropadd.check_major_league_errors(team) + logging.warning(f'Done checking data - checking WARa now ({data["wara"]})') + + if data['wara'] > 38.001 and not OFFSEASON_FLAG: + errors.append(f'- {dropadd.teams[team]["team"]["abbrev"]} would have {data["wara"]:.2f} sWAR') + + logging.warning(f'Now checking roster {len(data["roster"])}') + if len(data['roster']) > 26: + errors.append(f'- {dropadd.teams[team]["team"]["abbrev"]} would have {len(data["roster"])} players') + + logging.warning(f'Any errors? {errors}') + if (data['wara'] > 38.001 and not OFFSEASON_FLAG) or len(data['roster']) > 26: + roster_string = '' + for x in data['roster']: + roster_string += f'{data["roster"][x]["wara"]: >5} - {data["roster"][x]["name"]}\n' + errors.append(f'- This is the roster I have for {dropadd.teams[team]["team"]["abbrev"]}:\n' + f'```\n{roster_string}```') + + if len(data['mil_roster']) > 9: + errors.append( + f'- {dropadd.teams[team]["team"]["abbrev"]}MiL would have {len(data["mil_roster"])} players' + ) + + if len(errors) > 0: + error_string = "\n".join(errors) + await dropadd.send(f'Woof. Gotta say no to this one. Below is why.\n\n{error_string}') + await dropadd.timed_delete() + return + + # Ask for confirmation + for team in dropadd.teams: + this_q.prompt = f'{dropadd.teams[team]["role"].mention}\nWould you like me to run this move?' + this_q.qtype = 'yesno' + resp = await this_q.ask(dropadd.get_gms(self.bot, team)) + + if not resp: + await dropadd.send('RIP this move. Maybe next time.') + await dropadd.timed_delete() + return + else: + await dropadd.show_moves() + + # Run moves + trans_id = await dropadd.send_transaction() + + if not current['freeze'] or dropadd.avoid_freeze: + await send_to_channel(self.bot, 'transaction-log', embed=await dropadd.show_moves(here=False)) + + await dropadd.send(f'All done! Your transaction id is: {trans_id}') + await dropadd.timed_delete() + + @commands.command(name='ilmove', help='IL move') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME) + async def il_move_command(self, ctx): + current = await get_current() + + if OFFSEASON_FLAG: + await ctx.send(await get_emoji(ctx, 'oof', False)) + await ctx.send(f'I\'m not supposed to let anybody make IL moves during the offseason. You can still ' + f'trade and drop players to FA, though!') + return + + team = await get_team_by_owner(current['season'], ctx.author.id) + team_role = get_team_role(ctx, team) + player_cog = self.bot.get_cog('Players') + poke_role = get_role(ctx, 'Pokétwo') + + overwrites = {ctx.guild.default_role: discord.PermissionOverwrite(read_messages=False), + team_role: discord.PermissionOverwrite(read_messages=True), + ctx.guild.me: discord.PermissionOverwrite(read_messages=True), + poke_role: discord.PermissionOverwrite(read_messages=True, send_messages=False)} + + try: + t_channel = await ctx.guild.create_text_channel( + f'{team["abbrev"]}-transaction', + overwrites=overwrites, + category=discord.utils.get(ctx.guild.categories, name=f'Transactions') + ) + except Exception as e: + await ctx.send(f'{e}\n\n' + f'Discord is having issues creating private channels right now. Please try again later.') + return + + dropadd = SBaTransaction(t_channel, current, 'dropadd', this_week=True, first_team=team, team_role=team_role) + + await dropadd.send(f'Let\'s start here, {team_role.mention}') + await ctx.send(f'Take my hand... {dropadd.channel.mention}') + await dropadd.send(f'This transaction is for __this week__. It will go into effect for week {current["week"]}.') + + # Get IL moves + while True: + prompt = f'Are you sending someone to the IL?' + this_q = Question(self.bot, dropadd.channel, prompt, 'yesno', 300) + resp = await this_q.ask(dropadd.get_gms(self.bot)) + + if resp is None: + await dropadd.send('RIP this move. Maybe next time.') + await dropadd.timed_delete() + return + elif not resp: + break + else: + this_q.prompt = 'Who are you sending to the IL?' + this_q.qtype = 'text' + resp = await this_q.ask(dropadd.get_gms(self.bot)) + + if resp is None: + await dropadd.send('RIP this move. Maybe next time.') + await dropadd.timed_delete() + return + else: + try: + player = await get_one_player( + await fuzzy_player_search(ctx, dropadd.channel, self.bot, resp, + player_cog.player_list.keys()) + ) + except ValueError: + await dropadd.send(f'{await get_emoji(ctx, "squint")}') + await dropadd.send('Who even is that? Try again.') + else: + if not dropadd.included_team(player['team']): + await t_channel.send(f'It looks like {player["name"]} is on {player["team"]["abbrev"]} ' + f'so I can\'t let you do that.') + else: + dest_team = await get_one_team(f'{team["abbrev"]}IL') + await dropadd.add_player(player, dest_team) + + await dropadd.show_moves() + + # Get Major League Adds + while True and not OFFSEASON_FLAG: + prompt = f'Are you adding someone to the Major League team?' + this_q = Question(self.bot, dropadd.channel, prompt, 'yesno', 300) + resp = await this_q.ask(dropadd.get_gms(self.bot)) + + if resp is None: + await dropadd.send('RIP this move. Maybe next time.') + await dropadd.timed_delete() + return + elif not resp: + break + else: + this_q.prompt = 'Who are you adding?' + this_q.qtype = 'text' + resp = await this_q.ask(dropadd.get_gms(self.bot)) + + if resp is None: + await dropadd.send('RIP this move. Maybe next time.') + await dropadd.timed_delete() + return + else: + try: + player = await get_one_player( + await fuzzy_player_search(ctx, dropadd.channel, self.bot, resp, + player_cog.player_list.keys()) + ) + except ValueError: + await dropadd.send(f'{await get_emoji(ctx, "squint")}') + await dropadd.send('Who even is that? Try again.') + else: + if not self.on_team_il(team, player): + await t_channel.send(f'It looks like {player["name"]} is on {player["team"]["abbrev"]} ' + f'- you can only call up your MiL players mid-week.') + else: + await dropadd.add_player(player, team) + + await dropadd.show_moves() + + # Get MiL moves + while True and not OFFSEASON_FLAG: + prompt = f'Are you adding someone to the Minor League team?' + this_q = Question(self.bot, dropadd.channel, prompt, 'yesno', 300) + resp = await this_q.ask(dropadd.get_gms(self.bot)) + + if resp is None: + await dropadd.send('RIP this move. Maybe next time.') + await dropadd.timed_delete() + return + elif not resp: + break + else: + this_q.prompt = 'Who are you sending to the MiL?' + this_q.qtype = 'text' + resp = await this_q.ask(dropadd.get_gms(self.bot)) + + if resp is None: + await dropadd.send('RIP this move. Maybe next time.') + await dropadd.timed_delete() + return + else: + try: + player = await get_one_player( + await fuzzy_player_search(ctx, dropadd.channel, self.bot, resp, + player_cog.player_list.keys()) + ) + except ValueError: + await dropadd.send(f'{await get_emoji(ctx, "squint")}') + await dropadd.send('Who even is that? Try again.') + else: + if not dropadd.included_team(player['team']): + await t_channel.send(f'It looks like {player["name"]} is on {player["team"]["abbrev"]} ' + f'so I can\'t let you do that.') + # elif player['demotion_week'] > current['week']: + # await dropadd.send(f'Oof. {player["name"]} cannot be dropped until week ' + # f'{player["demotion_week"]}.') + else: + dest_team = await get_one_team(f'{team["abbrev"]}MiL') + await dropadd.add_player(player, dest_team) + + await dropadd.show_moves() + + # Get FA Drops + while True: + prompt = f'Are you dropping anyone to FA?' + this_q = Question(self.bot, dropadd.channel, prompt, 'yesno', 300) + resp = await this_q.ask(dropadd.get_gms(self.bot)) + + if resp is None: + await dropadd.send('RIP this move. Maybe next time.') + await dropadd.timed_delete() + return + elif not resp: + break + else: + this_q.prompt = 'Who are you dropping to FA?' + this_q.qtype = 'text' + resp = await this_q.ask(dropadd.get_gms(self.bot)) + + if resp is None: + await dropadd.send('RIP this move. Maybe next time.') + await dropadd.timed_delete() + return + else: + try: + player = await get_one_player( + await fuzzy_player_search(ctx, dropadd.channel, self.bot, resp, + player_cog.player_list.keys()) + ) + except ValueError: + await dropadd.send(f'{await get_emoji(ctx, "squint")}') + await dropadd.send('Who even is that? Try again.') + else: + if not dropadd.included_team(player['team']): + await t_channel.send(f'It looks like {player["name"]} is on {player["team"]["abbrev"]} ' + f'so I can\'t let you do that.') + # elif player['demotion_week'] > current['week']: + # await dropadd.send(f'Oof. {player["name"]} cannot be dropped until week ' + # f'{player["demotion_week"]}.') + else: + dest_team = await get_one_team('FA') + await dropadd.add_player(player, dest_team) + + await dropadd.show_moves() + + # Check for empty move + if len(dropadd.players) == 0: + await dropadd.send(f'This has been fun. Come again and maybe do something next time.') + await dropadd.timed_delete() + return + + # Check legality + errors = [] + for team in dropadd.teams: + data = await dropadd.check_major_league_errors(team) + + if data['wara'] > 38.001: + errors.append(f'- {dropadd.teams[team]["team"]["abbrev"]} would have {data["wara"]:.2f} WARa') + + if len(data['roster']) > 26: + errors.append(f'- {dropadd.teams[team]["team"]["abbrev"]} would have {len(data["roster"])} players') + + if data['wara'] > 38.001 or len(data['roster']) > 26: + roster_string = '' + for x in data['roster']: + roster_string += f'{data["roster"][x]["wara"]: >5} - {data["roster"][x]["name"]}\n' + errors.append(f'- This is the roster I have for {dropadd.teams[team]["team"]["abbrev"]}:\n' + f'```\n{roster_string}```') + + if len(errors) > 0: + error_string = "\n".join(errors) + await dropadd.send(f'Woof. Gotta say no to this one. Below is why.\n\n{error_string}') + await dropadd.timed_delete() + return + + # Ask for confirmation + for team in dropadd.teams: + this_q.prompt = f'{dropadd.teams[team]["role"].mention}\nWould you like me to run this move?' + this_q.qtype = 'yesno' + resp = await this_q.ask(dropadd.get_gms(self.bot, team)) + + if not resp: + await dropadd.send('RIP this move. Maybe next time.') + await dropadd.timed_delete() + return + else: + await dropadd.show_moves() + + # Post moves + trans_id = await dropadd.send_transaction() + + if dropadd.avoid_freeze: + # Run moves + for player_move in [*dropadd.players.values()]: + this_guy = copy.deepcopy(player_move['player']) + this_guy['team'] = player_move['to'] + await patch_player( + player_move['player']['id'], + team_id=player_move['to']['id'], + demotion_week=current['week'] + ) + + await send_to_channel(self.bot, 'transaction-log', embed=await dropadd.show_moves(here=False)) + await self.update_roster_sheet(current['season']) + + await dropadd.send(f'All done! Your transaction id is: {trans_id}') + await dropadd.timed_delete() + + @commands.command(name='mymoves', help='Show upcoming moves') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME) + async def my_moves_command(self, ctx): + current = await get_current() + team = await get_team_by_owner(current['season'], ctx.author.id) + set_moves = await get_transactions( + current['season'], + team_abbrev=team['abbrev'], + week_start=current['week']+1, + week_end=current['week']+1 + ) + frozen_moves = await get_transactions( + current['season'], + team_abbrev=team['abbrev'], + week_start=current['week']+1, + week_end=current['week']+1, + frozen=True + ) + logging.info(f'Num Moves: {len(set_moves)}') + + embed = get_team_embed(f'{team["lname"]} Guaranteed Transactions', team=team) + embed.description = f'Week {current["week"] + 1} Moves' + guaranteed = {} + frozen = {} + + for x in set_moves: + if set_moves[x]["moveid"] not in guaranteed.keys(): + guaranteed[set_moves[x]["moveid"]] = [] + + guaranteed[set_moves[x]["moveid"]].append( + f'**{set_moves[x]["player"]["name"]}** ({set_moves[x]["player"]["wara"]}) from ' + f'{set_moves[x]["oldteam"]["abbrev"]} to {set_moves[x]["newteam"]["abbrev"]}\n' + ) + + for x in frozen_moves: + if frozen_moves[x]["moveid"] not in frozen.keys(): + frozen[frozen_moves[x]["moveid"]] = [] + + frozen[frozen_moves[x]["moveid"]].append( + f'**{frozen_moves[x]["player"]["name"]}** ({frozen_moves[x]["player"]["wara"]}) from ' + f'{frozen_moves[x]["oldteam"]["abbrev"]} to {frozen_moves[x]["newteam"]["abbrev"]}\n' + ) + + if len(guaranteed) > 0: + for move_id in guaranteed: + move_string = ''.join(guaranteed[move_id]) + embed.add_field(name=f'Trans ID: {move_id}', value=move_string) + + if len(frozen) > 0: + for move_id in frozen: + move_string = ''.join(frozen[move_id]) + embed.add_field(name=f'Trans ID: {move_id}', value=move_string) + + await ctx.author.send(content='Hey, boo. Here are your upcoming transactions:', embed=embed) + await ctx.author.send('See everything you expected?') + + @commands.command(name='legal', help='Check roster legality') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME) + async def legal_command(self, ctx, *team_abbrev): + current = await get_current() + if team_abbrev: + this_team = await get_one_team(team_abbrev[0]) + else: + this_team = await get_team_by_owner(current['season'], ctx.author.id) + + this_week_team = await get_team_roster(this_team, 'current') + next_week_team = await get_team_roster(this_team, 'next') + + count = 0 + all_players = [] + + for roster in [this_week_team, next_week_team]: + embed = get_team_embed(f'{this_team["lname"]} Roster Check', team=this_team) + if count == 0: + embed.description = f'Week {current["week"]}' + else: + embed.description = f'Week {current["week"] + 1}' + + errors = [] + + sil_wara = roster['shortil']['WARa'] + total_wara = roster['active']['WARa'] + wara_string = f'{total_wara:.2f}' + if sil_wara > 0: + wara_string += f' ({sil_wara:.2f} IL)' + + embed.add_field(name='WARa', value=wara_string) + if total_wara > 38.001: + errors.append(f'- WARa currently {total_wara:.2f} (cap 38.0)') + + player_count = len(roster["active"]["players"]) + embed.add_field(name='Player Count', value=f'{player_count}') + if player_count != 26: + errors.append(f'- Currently have {player_count} players (need 26)') + + pos_string = f'```\nC 1B 2B 3B SS\n' \ + f'{roster["active"]["C"]} {roster["active"]["1B"]} {roster["active"]["2B"]} ' \ + f'{roster["active"]["3B"]} {roster["active"]["SS"]}\n\n' \ + f'LF CF RF SP RP\n' \ + f'{roster["active"]["LF"]} {roster["active"]["CF"]} {roster["active"]["RF"]} ' \ + f'{roster["active"]["SP"]} {roster["active"]["RP"]}\n```' + embed.add_field(name='Position Checks', value=pos_string, inline=False) + for pos in roster['active']: + if pos in ['C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF']: + logging.info(f'Pos: {pos} / {roster["active"][pos]}') + if roster["active"][pos] < 2: + errors.append(f'- Only have {roster["active"][pos]} {pos} (need 2)') + # elif pos in ['SP', 'RP']: + # logging.info(f'Pos: {pos} / {roster["active"][pos]}') + # if roster["active"][pos] < 5: + # errors.append(f'- Only have {roster["active"][pos]} {pos} (need 5)') + + if len(errors) > 0: + embed.add_field(name='Legality: ILLEGAL ❌', value="\n".join(errors), inline=False) + else: + embed.add_field(name='Legality: LEGAL 👍️', value='All good!', inline=False) + + await ctx.send(content=None, embed=embed) + count += 1 + + @commands.command(name='import', hidden=True) + @commands.is_owner() + async def import_command(self, ctx, sheet_id, tab_name, test=False): + # Get data from Sheets + async with ctx.typing(): + sheets = pygsheets.authorize(service_file='storage/major-domo-service-creds.json') + try: + data_sheet = sheets.open_by_key(sheet_id).worksheet_by_title(tab_name) + if test: + raw_data = data_sheet.get_values('A2', 'C5') + else: + raw_data = data_sheet.get_values('A2', 'C1100') + except Exception as e: + logging.error(f'{e}') + await ctx.send(f'Yikes. I need a grown-up to read this. I don\'t know what it means:\n\n{e}') + return + else: + await ctx.send(f'Noice - got it! Give me a minute to go through this data...') + + error_players = [] + tba_players = [] + fa_team = await get_one_team('FA', 5) + + async with ctx.typing(): + for row in raw_data: + if row[0] != '': + name = row[0] + swar = row[2] + + try: + old_player = await get_one_player(name) + except ValueError: + error_players.append(name) + else: + new_player = copy.deepcopy(old_player) + new_player['wara'] = swar + new_player['season'] = 5 + new_player['team_id'] = fa_team['id'] + del new_player['il_return'] + del new_player['demotion_week'] + del new_player['last_game'] + del new_player['last_game2'] + tba_players.append(new_player) + + await ctx.send(f'Okay, just read through that sheet. I\'ve got {len(tba_players)} players to add and have ' + f'the following errors: {", ".join(error_players)}') + + logging.info(f'tba_players:\n{tba_players}') + + if len(tba_players) > 0: + async with ctx.typing(): + done = await post_players(tba_players) + if done: + await ctx.send('All done!') + + @commands.command(name='tomil', help='Post-draft demotions') + @commands.has_any_role(SBA_PLAYERS_ROLE_NAME) + async def to_mil_command(self, ctx, *player_list): + current = await get_current() + team = await get_team_by_owner(current['season'], owner_id=ctx.author.id) + il_team = await get_one_team(f'{team["abbrev"]}MiL') + + if current['week'] != 0: + logging.info('entering the thot check') + await react_and_reply(ctx, '👀', 'https://c.tenor.com/FCAj8xDvEHwAAAAC/be-gone-thot.gif') + player_role = get_role(ctx, SBA_PLAYERS_ROLE_NAME) + bonked_role = get_role(ctx, 'BAINSHED') + logging.info('beginning sleep') + await asyncio.sleep(3) + + # try: + # await ctx.author.add_roles(bonked_role) + # except Exception as e: + # logging.error(f'unable to add {bonked_role} role to {ctx.author}: {e}') + try: + await ctx.author.remove_roles(player_role) + except Exception as e: + logging.error(f'unable to remove {player_role} role from {ctx.author}: {e}') + + return + + player_list = ' '.join(player_list) + player_names = re.split(',', player_list) + output_string = '' + errors = [] + moves = [] + + for x in player_names: + player_cog = self.bot.get_cog('Players') + + try: + player_name = await fuzzy_player_search(ctx, ctx.channel, self.bot, x, player_cog.player_list.keys()) + player = await get_one_player(player_name) + except Exception as e: + logging.error(f'Could not demote {x} for {team["abbrev"]}: {e}') + errors.append(x) + else: + if player['team']['id'] != team['id']: + await ctx.send(f'Omg stop trying to make {player["name"]} happen. It\'s not going to happen.') + else: + output_string += f'**{player["name"]}** ({player["wara"]}) to MiL\n' + if player['team']['id'] == team['id']: + moves.append({ + 'week': 1, + 'player_id': player['id'], + 'oldteam_id': team['id'], + 'newteam_id': il_team['id'], + 'season': current['season'], + 'moveid': f'draft-tomil-{team["abbrev"]}' + }) + + if len(moves) == 0: + await react_and_reply(ctx, '🖕', 'Thanks for that. So much fun.') + return + + await post_transactions(moves) + embed = get_team_embed(f'Pre-Season Demotions', team) + embed.add_field(name='Demotions', value=output_string, inline=False) + await send_to_channel(self.bot, 'transaction-log', content=None, embed=embed) + + if len(moves) > 0: + await react_and_reply(ctx, '✅', random_conf_gif()) + if len(errors) > 0: + await react_and_reply(ctx, '❌', f'I wasn\'t able to find {", ".join(errors)}') + + @commands.command(name='calisamazing', help='Make up for your idiocy') + async def cal_is_amazing_command(self, ctx): + current = await get_current() + team = await get_team_by_owner(current['season'], owner_id=ctx.author.id) + + if not team: + await ctx.send(random_conf_gif()) + + player_role = get_role(ctx, SBA_PLAYERS_ROLE_NAME) + try: + await ctx.author.add_roles(player_role) + except Exception as e: + logging.error(f'unable to add {player_role} role to {ctx.author}: {e}') + await react_and_reply( + ctx, '😩', + f'{e}\n\nLooks like you didn\'t apologize hard enough. I can\'t role you back up.') + await react_and_reply(ctx, '💖', 'You are too kind.') + + @commands.command(name='force_roster', help='Mod: Force update roster sheet') + async def force_roster_command(self, ctx): + current = await get_current() + message = await ctx.send('On it...') + fake_move = SBaTransaction(ctx.channel, current, 'dropadd') + await self.update_roster_sheet(season=current['season']) + await message.edit(content=new_rand_conf_gif()) + + +async def setup(bot): + await bot.add_cog(Transactions(bot)) diff --git a/db_calls.py b/db_calls.py new file mode 100644 index 0000000..8587fa4 --- /dev/null +++ b/db_calls.py @@ -0,0 +1,1099 @@ +import requests +import logging +import json +import os +from bs4 import BeautifulSoup +import csv + + +AUTH_TOKEN = {'Authorization': f'Bearer {os.environ.get("API_TOKEN")}'} +DB_URL = 'http://database/api' + + +def param_char(other_params): + if other_params: + return '&' + else: + return '?' + + +def get_req_url(endpoint: str, api_ver: int = 1, object_id: str = None, params: list = None): + req_url = f'{DB_URL}/v{api_ver}/{endpoint}{"/" if object_id is not None else ""}{object_id if object_id is not None else ""}' + + if params: + other_params = False + for x in params: + req_url += f'{param_char(other_params)}{x[0]}={x[1]}' + other_params = True + + return req_url + + +def db_get(endpoint: str, api_ver: int = 1, object_id: int = None, params: list = None, as_csv: bool = False): + req_url = get_req_url(endpoint, api_ver=api_ver, object_id=object_id, params=params) + + resp = requests.get(req_url) + if resp.status_code == 200: + if as_csv: + return csv.reader(map(bytes.decode, resp.content)) + return resp.json() + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_current(season=None): + req_url = f'http://database/api/v1/current' + if season: + req_url += f'?season={season}' + + resp = requests.get(req_url) + if resp.status_code == 200: + return resp.json() + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def patch_current( + week=None, freeze=None, season=None, transcount=None, bstatcount=None, pstatcount=None, bet_week=None, + trade_deadline=None, pick_trade_start=None, pick_trade_end=None, injury_count=None): + req_url = f'http://database/api/v1/current' + other_params = False + if week: + req_url += f'{param_char(other_params)}week={week}' + other_params = True + if freeze is not None: + req_url += f'{param_char(other_params)}freeze={freeze}' + other_params = True + if season: + req_url += f'{param_char(other_params)}season={season}' + other_params = True + if transcount: + req_url += f'{param_char(other_params)}transcount={transcount}' + other_params = True + if bstatcount: + req_url += f'{param_char(other_params)}bstatcount={bstatcount}' + other_params = True + if pstatcount: + req_url += f'{param_char(other_params)}pstatcount={pstatcount}' + other_params = True + if bet_week: + req_url += f'{param_char(other_params)}bet_week={bet_week}' + other_params = True + if trade_deadline: + req_url += f'{param_char(other_params)}trade_deadline={trade_deadline}' + other_params = True + if pick_trade_start: + req_url += f'{param_char(other_params)}pick_trade_start={pick_trade_start}' + other_params = True + if pick_trade_end: + req_url += f'{param_char(other_params)}pick_trade_end={pick_trade_end}' + other_params = True + if injury_count: + req_url += f'{param_char(other_params)}injury_count={injury_count}' + other_params = True + + resp = requests.patch(req_url, data=None, headers=AUTH_TOKEN, timeout=3) + if resp.status_code == 200: + return True + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_one_team(id_or_abbrev, season=None, is_pd=False, timeout=3): + if is_pd: + req_url = f'http://pd-database/api/v1/teams/{id_or_abbrev}' + else: + req_url = f'http://database/api/v1/teams/{id_or_abbrev}' + if season: + req_url += f'?season={season}' + + resp = requests.get(req_url, timeout=timeout) + + if resp.status_code == 200: + return resp.json() + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_team_by_owner(season, owner_id): + resp = requests.get(f'http://database/api/v1/teams?season={season}&owner_id={owner_id}&active_only=True', timeout=3) + if resp.status_code == 200: + full_resp = resp.json() + if len(full_resp['teams']) != 1: + raise ValueError(f'One team requested, but {len(full_resp)} were returned') + else: + return full_resp['teams'][0] + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def post_team(team): + req_url = f'http://database/api/v1/teams' + payload = json.dumps(team) + + resp = requests.post(req_url, payload, headers=AUTH_TOKEN, timeout=3) + if resp.status_code == 200: + return resp.json() + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_team_roster(team, current_or_next): + resp = requests.get(f'http://database/api/v1/teams/{team["id"]}/roster/{current_or_next}', timeout=3) + if resp.status_code == 200: + return resp.json() + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_team_record(team, week_num): + resp = requests.get(f'http://database/api/v1/teams/{team["id"]}/record/{week_num}', timeout=3) + if resp.status_code == 200: + return resp.json() + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def patch_team( + team, manager=None, gmid=None, gmid2=None, mascot=None, stadium=None, thumbnail=None, color=None, + dice_color=None): + req_url = f'http://database/api/v1/teams/{team["id"]}' + other_params = False + if manager: + req_url += f'{param_char(other_params)}manager={manager}' + other_params = True + if gmid: + req_url += f'{param_char(other_params)}gmid={gmid}' + other_params = True + if gmid2: + req_url += f'{param_char(other_params)}gmid2={gmid2}' + other_params = True + if mascot: + req_url += f'{param_char(other_params)}mascot={mascot}' + other_params = True + if stadium: + req_url += f'{param_char(other_params)}stadium={stadium}' + other_params = True + if thumbnail: + req_url += f'{param_char(other_params)}thumbnail={thumbnail}' + other_params = True + if color: + req_url += f'{param_char(other_params)}color={color}' + other_params = True + if dice_color: + req_url += f'{param_char(other_params)}dice_color={dice_color}' + other_params = True + + resp = requests.patch(req_url, data=None, headers=AUTH_TOKEN, timeout=3) + if resp.status_code == 200: + return True + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_schedule( + season, team_abbrev1=None, team_abbrev2=None, away_abbrev=None, home_abbrev=None, week_start=None, + week_end=None): + req_url = f'http://database/api/v1/schedules?season={season}' + if team_abbrev1: + req_url += f'&team_abbrev1={team_abbrev1}' + if team_abbrev2: + req_url += f'&team_abbrev2={team_abbrev2}' + if away_abbrev: + req_url += f'&away_abbrev={away_abbrev}' + if home_abbrev: + req_url += f'&home_abbrev={home_abbrev}' + if week_start: + req_url += f'&week_start={week_start}' + if week_end: + req_url += f'&week_end={week_end}' + + resp = requests.get(req_url, timeout=3) + if resp.status_code == 200: + return resp.json() + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_one_schedule( + season, team_abbrev1=None, team_abbrev2=None, away_abbrev=None, home_abbrev=None, week=None): + req_url = f'http://database/api/v1/schedules?season={season}' + if team_abbrev1: + req_url += f'&team_abbrev1={team_abbrev1}' + if team_abbrev2: + req_url += f'&team_abbrev2={team_abbrev2}' + if away_abbrev: + req_url += f'&away_abbrev={away_abbrev}' + if home_abbrev: + req_url += f'&home_abbrev={home_abbrev}' + if week: + req_url += f'&week_start={week}' + req_url += f'&week_end={week}' + + resp = requests.get(req_url, timeout=3) + if resp.status_code == 200: + full_resp = resp.json() + if len(full_resp) > 2: + raise ValueError(f'One schedule requested, but {len(full_resp)} were returned') + elif len(full_resp) == 0: + raise ValueError(f'No schedules found') + else: + key = [*full_resp] + return_val = full_resp[key[0]] + return return_val + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def post_players(all_players: list): + req_url = f'http://database/api/v1/players' + data = { + 'players': all_players + } + payload = json.dumps(data) + + resp = requests.post(req_url, payload, headers=AUTH_TOKEN, timeout=3) + if resp.status_code == 200: + return True + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_players(season, team_abbrev=None, sort=None, injured=None): + req_url = f'http://database/api/v2/players?season={season}' + if team_abbrev: + req_url += f'&team_abbrev={team_abbrev}' + if sort: + req_url += f'&sort={sort}' + if injured: + req_url += f'&injured={injured}' + + resp = requests.get(req_url, timeout=3) + if resp.status_code == 200: + return resp.json() + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_one_player(id_or_name, season=None): + req_url = f'http://database/api/v1/players/{id_or_name}' + if season is not None: + req_url += f'?season={season}' + + resp = requests.get(req_url, timeout=3) + if resp.status_code == 200: + return resp.json() + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def patch_player( + pid, name=None, wara=None, image=None, image2=None, team_id=None, season=None, pitcher_injury=None, pos_1=None, + pos_2=None, pos_3=None, pos_4=None, pos_5=None, pos_6=None, pos_7=None, pos_8=None, last_game=None, + last_game2=None, il_return=None, demotion_week=None, headshot=None, vanity_card=None, injury_rating=None): + """ + For values being patched to "None", set to False + + :param pid: + :param name: + :param wara: + :param image: + :param image2: + :param team_id: + :param season: + :param pitcher_injury: + :param pos_1: + :param pos_2: + :param pos_3: + :param pos_4: + :param pos_5: + :param pos_6: + :param pos_7: + :param pos_8: + :param last_game: + :param last_game2: + :param il_return: + :return: + """ + req_url = f'http://database/api/v2/players/{pid}' + other_params = False + if name is not None: + req_url += f'{param_char(other_params)}name={name}' + other_params = True + if wara is not None: + req_url += f'{param_char(other_params)}wara={wara}' + other_params = True + if image is not None: + req_url += f'{param_char(other_params)}image={image}' + other_params = True + if image2 is not None: + if not image2: + req_url += f'{param_char(other_params)}image2=False' + else: + req_url += f'{param_char(other_params)}image2={image2}' + other_params = True + if team_id is not None: + req_url += f'{param_char(other_params)}team_id={team_id}' + other_params = True + if season is not None: + req_url += f'{param_char(other_params)}season={season}' + other_params = True + if pitcher_injury is not None: + if not pitcher_injury: + req_url += f'{param_char(other_params)}pitcher_injury=False' + else: + req_url += f'{param_char(other_params)}pitcher_injury={pitcher_injury}' + other_params = True + if pos_1 is not None: + req_url += f'{param_char(other_params)}pos_1={pos_1}' + other_params = True + if pos_2 is not None: + if not pos_2: + req_url += f'{param_char(other_params)}pos_2=False' + else: + req_url += f'{param_char(other_params)}pos_2={pos_2}' + other_params = True + if pos_3 is not None: + if not pos_3: + req_url += f'{param_char(other_params)}pos_3=False' + else: + req_url += f'{param_char(other_params)}pos_3={pos_3}' + other_params = True + if pos_4 is not None: + if not pos_4: + req_url += f'{param_char(other_params)}pos_4=False' + else: + req_url += f'{param_char(other_params)}pos_4={pos_4}' + other_params = True + if pos_5 is not None: + if not pos_5: + req_url += f'{param_char(other_params)}pos_5=False' + else: + req_url += f'{param_char(other_params)}pos_5={pos_5}' + other_params = True + if pos_6 is not None: + if not pos_6: + req_url += f'{param_char(other_params)}pos_6=False' + else: + req_url += f'{param_char(other_params)}pos_6={pos_6}' + other_params = True + if pos_7 is not None: + if not pos_7: + req_url += f'{param_char(other_params)}pos_7=False' + else: + req_url += f'{param_char(other_params)}pos_7={pos_7}' + other_params = True + if pos_8 is not None: + if not pos_8: + req_url += f'{param_char(other_params)}pos_8=False' + else: + req_url += f'{param_char(other_params)}pos_8={pos_8}' + other_params = True + if last_game is not None: + if not last_game: + req_url += f'{param_char(other_params)}last_game=False' + else: + req_url += f'{param_char(other_params)}last_game={last_game}' + other_params = True + if last_game2 is not None: + req_url += f'{param_char(other_params)}last_game2={last_game2}' + other_params = True + if il_return is not None: + if not il_return: + req_url += f'{param_char(other_params)}il_return=false' + else: + req_url += f'{param_char(other_params)}il_return={il_return}' + other_params = True + if demotion_week is not None: + req_url += f'{param_char(other_params)}demotion_week={demotion_week}' + other_params = True + if headshot is not None: + if not headshot: + req_url += f'{param_char(other_params)}headshot=false' + else: + req_url += f'{param_char(other_params)}headshot={headshot}' + other_params = True + if vanity_card is not None: + if not vanity_card: + req_url += f'{param_char(other_params)}vanity_card=false' + else: + req_url += f'{param_char(other_params)}vanity_card={vanity_card}' + other_params = True + if injury_rating is not None: + req_url += f'{param_char(other_params)}injury_rating={injury_rating}' + other_params = True + + resp = requests.patch(req_url, headers=AUTH_TOKEN, timeout=3) + if resp.status_code == 200: + return True + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_standings(season=None, team_abbrev=None, league_abbrev=None, division_abbrev=None): + req_url = f'http://database/api/v1/standings' + other_params = False + if season: + req_url += f'{param_char(other_params)}season={season}' + other_params = True + if team_abbrev: + req_url += f'{param_char(other_params)}team_abbrev={team_abbrev}' + other_params = True + if league_abbrev: + req_url += f'{param_char(other_params)}league_abbrev={league_abbrev}' + other_params = True + if division_abbrev: + req_url += f'{param_char(other_params)}division_abbrev={division_abbrev}' + other_params = True + + resp = requests.get(req_url, timeout=3) + if resp.status_code == 200: + return resp.json() + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def post_standings_recalc(season): + req_url = f'http://database/api/v1/standings/s{season}/recalculate' + + resp = requests.post(req_url, headers=AUTH_TOKEN, timeout=15) + if resp.status_code == 200: + print(f'Done calculating') + return resp.json() + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_results(season, team_abbrev=None, week=None, away_abbrev=None, home_abbrev=None): + req_url = f'http://database/api/v1/results?season={season}' + if team_abbrev: + req_url += f'&team_abbrev={team_abbrev}' + if week: + req_url += f'&week={week}' + if away_abbrev: + req_url += f'&away_abbrev={away_abbrev}' + if home_abbrev: + req_url += f'&home_abbrev={home_abbrev}' + + resp = requests.get(req_url, timeout=3) + if resp.status_code == 200: + return resp.json() + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def post_result(result): + req_url = f'http://database/api/v1/results' + payload = json.dumps(result) + + resp = requests.post(req_url, payload, headers=AUTH_TOKEN, timeout=3) + if resp.status_code == 200: + return True + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def patch_result(result_id, away_score=None, home_score=None): + req_url = f'http://database/api/v1/results/{result_id}' + other_params = False + if away_score: + req_url += f'{param_char(other_params)}away_score={away_score}' + other_params = True + if home_score: + req_url += f'{param_char(other_params)}home_score={home_score}' + other_params = True + + resp = requests.patch(req_url, headers=AUTH_TOKEN, timeout=3) + if resp.status_code == 200: + return True + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def delete_result(result_id): + req_url = f'http://database/api/v1/results/{result_id}' + + resp = requests.delete(req_url, headers=AUTH_TOKEN, timeout=3) + if resp.status_code == 200: + return True + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_transactions( + season, team_abbrev=None, week_start=None, week_end=None, cancelled=None, frozen=None, + player_name=None, player_id=None, move_id=None, timeout=3): + req_url = f'http://database/api/v1/transactions?season={season}' + if team_abbrev: + req_url += f'&team_abbrev={team_abbrev}' + if week_start: + req_url += f'&week_start={week_start}' + if week_end: + req_url += f'&week_end={week_end}' + if cancelled: + req_url += f'&cancelled={cancelled}' + if frozen: + req_url += f'&frozen={frozen}' + if player_name: + req_url += f'&player_name={player_name}' + if player_id: + req_url += f'&player_id={player_id}' + if move_id: + req_url += f'&move_id={move_id}' + + resp = requests.get(req_url, timeout=timeout) + if resp.status_code == 200: + return resp.json() + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def post_transactions(moves: list): + req_url = f'http://database/api/v1/transactions' + data = { + 'count': len(moves), + 'moves': moves + } + payload = json.dumps(data) + + resp = requests.post(req_url, payload, headers=AUTH_TOKEN, timeout=3) + if resp.status_code == 200: + return True + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def patch_transaction(move_id, frozen=None, cancelled=None): + req_url = f'http://database/api/v1/transactions/{move_id}' + other_params = False + if frozen is not None: + req_url += f'{param_char(other_params)}frozen={frozen}' + other_params = True + if cancelled is not None: + req_url += f'{param_char(other_params)}cancelled={cancelled}' + other_params = True + + resp = requests.patch(req_url, headers=AUTH_TOKEN, timeout=3) + if resp.status_code == 200: + return True + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_one_battingseason(player_id): + req_url = f'http://database/api/v1/battingseasons/{player_id}' + + resp = requests.get(req_url, timeout=3) + if resp.status_code == 200: + return resp.json() + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_one_pitchingseason(player_id): + req_url = f'http://database/api/v1/pitchingseasons/{player_id}' + + resp = requests.get(req_url, timeout=3) + if resp.status_code == 200: + return resp.json() + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_one_fieldingseason(player_id): + req_url = f'http://database/api/v1/fieldingseasons/{player_id}' + + resp = requests.get(req_url, timeout=3) + if resp.status_code == 200: + return resp.json() + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_battingcareer(name_or_id): + req_url = f'http://database/api/v1/battingcareer/{name_or_id}' + + resp = requests.get(req_url, timeout=3) + if resp.status_code == 200: + return resp.json() + elif resp.status_code == 404: + return None + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_pitchingcareer(name_or_id): + req_url = f'http://database/api/v1/pitchingcareer/{name_or_id}' + + resp = requests.get(req_url, timeout=3) + if resp.status_code == 200: + return resp.json() + elif resp.status_code == 404: + return None + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_battingstat( + season, s_type='regular', team_abbrev=None, player_name=None, player_id=None, week_start=None, + week_end=None, game_num=None, position=None, timeout=10): + req_url = f'http://database/api/v1/battingstats/s{season}/{s_type}' + other_params = False + if team_abbrev: + req_url += f'{param_char(other_params)}team_abbrev={team_abbrev}' + other_params = True + if player_name: + req_url += f'{param_char(other_params)}player_name={player_name}' + other_params = True + if player_id: + req_url += f'{param_char(other_params)}player_id={player_id}' + other_params = True + if week_start: + req_url += f'{param_char(other_params)}week_start={week_start}' + other_params = True + if week_end: + req_url += f'{param_char(other_params)}week_end={week_end}' + other_params = True + if game_num: + req_url += f'{param_char(other_params)}game_num={game_num}' + other_params = True + if position: + req_url += f'{param_char(other_params)}position={position}' + other_params = True + + resp = requests.get(req_url, timeout=timeout) + if resp.status_code == 200: + return resp.json() + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def post_battingstats(stats: list): + req_url = f'http://database/api/v1/battingstats' + data = { + 'count': len(stats), + 'stats': stats + } + payload = json.dumps(data) + + resp = requests.post(req_url, payload, headers=AUTH_TOKEN, timeout=3) + if resp.status_code == 200: + return True + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def delete_battingstats(game_obj: dict): + req_url = f'http://database/api/v1/battingstats' + payload = json.dumps(game_obj) + + resp = requests.delete(req_url, data=payload, headers=AUTH_TOKEN) + if resp.status_code == 200: + return True + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def recalc_batting_seasons(season, team_id): + req_url = f'http://database/api/v1/battingseasons/recalculate?season={season}&team_id={team_id}' + + resp = requests.post(req_url, headers=AUTH_TOKEN, timeout=45) + if resp.status_code == 200: + return True + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def recalc_fielding_seasons(season, team_id): + req_url = f'http://database/api/v1/fieldingseasons/recalculate?season={season}&team_id={team_id}' + + resp = requests.post(req_url, headers=AUTH_TOKEN, timeout=45) + if resp.status_code == 200: + return True + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_pitchingstat( + season, s_type='regular', team_abbrev=None, player_name=None, player_id=None, week_start=None, + week_end=None, game_num=None, timeout=10): + req_url = f'http://database/api/v1/pitchingstats/s{season}/{s_type}' + other_params = False + if team_abbrev: + req_url += f'{param_char(other_params)}team_abbrev={team_abbrev}' + other_params = True + if player_name: + req_url += f'{param_char(other_params)}player_name={player_name}' + other_params = True + if player_id: + req_url += f'{param_char(other_params)}player_id={player_id}' + other_params = True + if week_start: + req_url += f'{param_char(other_params)}week_start={week_start}' + other_params = True + if game_num: + req_url += f'{param_char(other_params)}game_num={game_num}' + other_params = True + if week_end: + req_url += f'{param_char(other_params)}week_end={week_end}' + other_params = True + + resp = requests.get(req_url, timeout=timeout) + if resp.status_code == 200: + return resp.json() + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def post_pitchingstats(stats: list): + req_url = f'http://database/api/v1/pitchingstats' + data = { + 'count': len(stats), + 'stats': stats + } + payload = json.dumps(data) + + resp = requests.post(req_url, payload, headers=AUTH_TOKEN, timeout=3) + if resp.status_code == 200: + return True + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def delete_pitchingstats(game_obj: dict): + req_url = f'http://database/api/v1/pitchingstats' + payload = json.dumps(game_obj) + + resp = requests.delete(req_url, data=payload, headers=AUTH_TOKEN) + if resp.status_code == 200: + return True + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def recalc_pitching_seasons(season, team_id): + req_url = f'http://database/api/v1/pitchingseasons/recalculate?season={season}&team_id={team_id}' + + resp = requests.post(req_url, headers=AUTH_TOKEN, timeout=45) + if resp.status_code == 200: + return True + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_draftdata(): + resp = requests.get(f'http://database/api/v1/draftdata') + if resp.status_code == 200: + return resp.json() + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def patch_draftdata( + currentpick=None, timer=None, pick_deadline=None, result_channel=None, ping_channel=None, pick_minutes=None): + req_url = f'http://database/api/v1/draftdata' + other_params = False + if currentpick is not None: + req_url += f'{param_char(other_params)}currentpick={currentpick}' + other_params = True + if timer is not None: + req_url += f'{param_char(other_params)}timer={timer}' + other_params = True + if pick_deadline is not None: + req_url += f'{param_char(other_params)}pick_deadline={pick_deadline}' + other_params = True + if result_channel is not None: + req_url += f'{param_char(other_params)}result_channel={result_channel}' + other_params = True + if ping_channel is not None: + req_url += f'{param_char(other_params)}ping_channel={ping_channel}' + other_params = True + if pick_minutes is not None: + req_url += f'{param_char(other_params)}pick_minutes={pick_minutes}' + other_params = True + + resp = requests.patch(req_url, headers=AUTH_TOKEN, timeout=3) + if resp.status_code == 200: + return True + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_draftpicks( + season, owner_team=None, orig_owner_team=None, round_start=None, round_end=None, team_season=None, + overall_start=None, overall_end=None): + req_url = f'http://database/api/v1/draftpicks?season={season}' + if owner_team: + req_url += f'&owner_team_abbrev={owner_team["abbrev"]}' + if orig_owner_team: + req_url += f'&orig_team_abbrev={orig_owner_team["abbrev"]}' + if round_start: + req_url += f'&pick_round_start={round_start}' + if round_end: + req_url += f'&pick_round_end={round_end}' + if team_season: + req_url += f'&team_season={team_season}' + if overall_start: + req_url += f'&overall_start={overall_start}' + if overall_end: + req_url += f'&overall_end={overall_end}' + + resp = requests.get(req_url, timeout=3) + if resp.status_code == 200: + return resp.json() + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_one_draftpick_search(season, orig_owner_abbrev, round_num: int, team_season: int = None): + req_url = f'http://database/api/v1/draftpicks?season={season}' + req_url += f'&orig_team_abbrev={orig_owner_abbrev}' + req_url += f'&pick_round_start={round_num}' + req_url += f'&pick_round_end={round_num}' + if team_season is not None: + req_url += f'&team_season={team_season}' + + resp = requests.get(req_url, timeout=3) + if resp.status_code == 200: + picks = resp.json() + if len(picks) == 0: + return None + elif len(picks) == 1: + for x in picks: + return picks[x] + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_one_draftpick_byid(pick_id: int): + req_url = f'http://database/api/v1/draftpicks/{pick_id}' + + resp = requests.get(req_url, timeout=3) + if resp.status_code == 200: + return resp.json() + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_one_draftpick_byoverall(season: int, overall: int): + req_url = f'http://database/api/v1/draftpicks?season={season}&overall={overall}' + + resp = requests.get(req_url, timeout=3) + if resp.status_code == 200: + full_resp = resp.json() + if len(full_resp) != 1: + return None + else: + key = [*full_resp] + return full_resp[key[0]] + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def patch_draftpick( + pick_id: int, overall: int = None, round: int = None, orig_owner_id: int = None, owner_id: int = None, + season: int = None, player_id: int = None): + req_url = f'http://database/api/v1/draftpicks/{pick_id}' + other_params = False + if overall is not None: + req_url += f'{param_char(other_params)}overall={overall}' + other_params = True + if round is not None: + req_url += f'{param_char(other_params)}round={round}' + other_params = True + if orig_owner_id is not None: + req_url += f'{param_char(other_params)}orig_owner_id={orig_owner_id}' + other_params = True + if owner_id is not None: + req_url += f'{param_char(other_params)}owner_id={owner_id}' + other_params = True + if season is not None: + req_url += f'{param_char(other_params)}season={season}' + other_params = True + if player_id is not None: + req_url += f'{param_char(other_params)}player_id={player_id}' + other_params = True + + resp = requests.patch(req_url, headers=AUTH_TOKEN, timeout=3) + if resp.status_code == 200: + return True + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_awards(player_id=None, player_name=None, season=None): + req_url = f'http://database/api/v1/awards' + other_params = False + if player_id: + req_url += f'{param_char(other_params)}player_id={player_id}' + other_params = True + if player_name: + req_url += f'{param_char(other_params)}player_name={player_name}' + other_params = True + if season: + req_url += f'{param_char(other_params)}season={season}' + other_params = True + + resp = requests.get(req_url, timeout=3) + if resp.status_code == 200: + return resp.json() + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def post_award(award: dict): + req_url = f'http://database/api/v1/awards' + award_model = { + 'name': award['name'], + 'season': award['season'], + 'timing': award['timing'], + } + if award['manager1']: + award_model['manager1_id'] = award['manager1']['id'] + if award['manager2']: + award_model['manager2_id'] = award['manager2']['id'] + if award['player']: + award_model['player_id'] = award['player']['id'] + if award['team']: + award_model['team_id'] = award['team']['id'] + if award['image']: + award_model['image'] = award['image'] + + payload_prep = {'count': 1, 'awards': [award_model]} + payload = json.dumps(payload_prep) + + resp = requests.post(req_url, payload, headers=AUTH_TOKEN, timeout=3) + if resp.status_code == 200: + return True + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_one_manager(manager_name): + req_url = f'http://database/api/v1/managers/{manager_name}' + + resp = requests.get(req_url, timeout=3) + if resp.status_code == 200: + return resp.json() + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def post_dice(dice_bag: list): + req_url = f'http://database/api/v1/dice' + dice_model = { + 'count': len(dice_bag), + 'rolls': dice_bag + } + + payload = json.dumps(dice_model) + + resp = requests.post(req_url, payload, headers=AUTH_TOKEN, timeout=3) + if resp.status_code == 200: + return True + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_player_photo(player_name): + req_url = f'https://www.thesportsdb.com/api/v1/json/1/searchplayers.php?p={player_name}' + + try: + resp = requests.get(req_url, timeout=.5) + except Exception as e: + return None + if resp.status_code == 200 and resp.json()['player']: + if resp.json()['player'][0]['strSport'] == 'Baseball': + return resp.json()['player'][0]['strThumb'] + return None + + +async def get_player_headshot(player_name): + req_url = f'https://www.baseball-reference.com/search/search.fcgi?search={player_name}' + + try: + resp = requests.get(req_url, timeout=1).text + except Exception as e: + return None + soup = BeautifulSoup(resp, 'html.parser') + for item in soup.find_all('img'): + if 'headshot' in item['src']: + return item['src'] + return await get_player_photo(player_name) + + +async def post_draft_list(this_list: list): + req_url = f'http://database/api/v1/draft-list' + list_model = { + 'count': len(this_list), + 'draft_list': this_list + } + + payload = json.dumps(list_model) + + resp = requests.post(req_url, payload, headers=AUTH_TOKEN, timeout=3) + if resp.status_code == 200: + return True + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +async def get_draft_list(team): + req_url = f'http://database/api/v1/draft-list/{team["id"]}' + + resp = requests.get(req_url, headers=AUTH_TOKEN, timeout=3) + if resp.status_code == 200: + return resp.json() + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') diff --git a/db_calls_gameday.py b/db_calls_gameday.py new file mode 100644 index 0000000..983b44d --- /dev/null +++ b/db_calls_gameday.py @@ -0,0 +1,1049 @@ +import math +from typing import Optional +import pydantic +from peewee import * +from playhouse.shortcuts import model_to_dict +import logging + +db = SqliteDatabase( + 'storage/gameday.db', + pragmas={ + 'journal_mode': 'wal', + 'cache_size': -1 * 64000, + 'synchronous': 0 + } +) + + +class BaseModel(Model): + class Meta: + database = db + + +class Game(BaseModel): + away_team_id = IntegerField() + home_team_id = IntegerField() + week_num = IntegerField(null=True) + game_num = IntegerField(null=True) + channel_id = IntegerField() + active = BooleanField(default=True) + # is_pd = BooleanField(default=False) + + +class GameModel(pydantic.BaseModel): + away_team_id: int + home_team_id: int + week_num: Optional[int] = None + game_num: Optional[int] = None + channel_id: int + active: Optional[bool] = True + # is_pd: Optional[bool] = True + + +def post_game(game_dict: dict): + game_model = GameModel.parse_obj(game_dict) + new_game = Game( + away_team_id=game_model.away_team_id, + home_team_id=game_model.home_team_id, + week_num=game_model.week_num, + game_num=game_model.game_num, + channel_id=game_model.channel_id, + active=game_model.active, + # is_pd=game_model.is_pd + ) + new_game.save() + return_game = model_to_dict(new_game) + db.close() + + new_gs = post_game_state({'game_id': new_game.id}) + + return return_game + + +def get_game(away_team_id=None, home_team_id=None, week_num=None, game_num=None, channel_id=None, active=None): + this_game = Game.select() + if away_team_id is not None: + this_game = this_game.where(Game.away_team_id == away_team_id) + if home_team_id is not None: + this_game = this_game.where(Game.home_team_id == home_team_id) + if week_num is not None: + this_game = this_game.where(Game.week_num == week_num) + if game_num is not None: + this_game = this_game.where(Game.game_num == game_num) + if channel_id is not None: + this_game = this_game.where(Game.channel_id == channel_id) + if active is not None: + this_game = this_game.where(Game.active == active) + + if this_game.count() > 0: + return_game = model_to_dict(this_game[0]) + db.close() + return return_game + else: + raise DatabaseError('No game found') + + +def patch_game( + game_id, away_team_id=None, home_team_id=None, week_num=None, game_num=None, channel_id=None, active=None): + this_game = Game.get_by_id(game_id) + if away_team_id is not None: + this_game.away_team_id = away_team_id + if home_team_id is not None: + this_game.home_team_id = home_team_id + if week_num is not None: + this_game.week_num = week_num + if game_num is not None: + this_game.game_num = game_num + if channel_id is not None: + this_game.channel_id = channel_id + if active is not None: + this_game.active = active + this_game.save() + return_game = model_to_dict(this_game) + db.close() + return return_game + + +class AtBat(BaseModel): + game = ForeignKeyField(Game) + batter_id = IntegerField() + pitcher_id = IntegerField() + on_base_code = IntegerField() + inning = IntegerField() + pa = IntegerField() + ab = IntegerField() + run = IntegerField() + hit = IntegerField() + rbi = IntegerField() + double = IntegerField() + triple = IntegerField() + homerun = IntegerField() + bb = IntegerField() + so = IntegerField() + hbp = IntegerField() + sac = IntegerField() + ibb = IntegerField() + gidp = IntegerField() + bphr = IntegerField() + bpfo = IntegerField() + bp1b = IntegerField() + bplo = IntegerField() + error = IntegerField() + + +class AtBatModel(pydantic.BaseModel): + game_id: int + batter_id: int + pitcher_id: int + on_base_code: int + inning: int + pa: Optional[int] = 1 + ab: Optional[int] = 0 + run: Optional[int] = 0 + hit: Optional[int] = 0 + rbi: Optional[int] = 0 + double: Optional[int] = 0 + triple: Optional[int] = 0 + homerun: Optional[int] = 0 + bb: Optional[int] = 0 + so: Optional[int] = 0 + hbp: Optional[int] = 0 + sac: Optional[int] = 0 + ibb: Optional[int] = 0 + gidp: Optional[int] = 0 + bphr: Optional[int] = 0 + bpfo: Optional[int] = 0 + bp1b: Optional[int] = 0 + bplo: Optional[int] = 0 + error: Optional[int] = False + + +def post_atbat(ab_dict: dict): + this_ab = AtBatModel.parse_obj(ab_dict) + this_game = Game.get_by_id(this_ab.game_id) + new_ab = AtBat( + game=this_game, + batter_id=this_ab.batter_id, + pitcher_id=this_ab.pitcher_id, + on_base_code=this_ab.on_base_code, + inning=this_ab.inning, + pa=this_ab.pa, + ab=this_ab.ab, + run=this_ab.run, + hit=this_ab.hit, + rbi=this_ab.rbi, + double=this_ab.double, + triple=this_ab.triple, + homerun=this_ab.homerun, + bb=this_ab.bb, + so=this_ab.so, + hbp=this_ab.hbp, + sac=this_ab.sac, + ibb=this_ab.ibb, + gidp=this_ab.gidp, + bphr=this_ab.bphr, + bpfo=this_ab.bpfo, + bp1b=this_ab.bp1b, + bplo=this_ab.bplo, + error=this_ab.error + ) + new_ab.save() + return_ab = model_to_dict(new_ab) + db.close() + return return_ab + + +def get_atbat(game_id=None, batter_id=None, pitcher_id=None, on_base_code=None, inning=None, error=None): + this_ab = AtBat.select() + if game_id is not None: + this_game = Game.get_by_id(game_id) + this_ab = this_ab.where(AtBat.game == this_game) + if batter_id is not None: + this_ab = this_ab.where(AtBat.batter_id == batter_id) + if pitcher_id is not None: + this_ab = this_ab.where(AtBat.pitcher_id == pitcher_id) + if on_base_code is not None: + this_ab = this_ab.where(AtBat.on_base_code == on_base_code) + if inning is not None: + this_ab = this_ab.where(AtBat.inning == inning) + if inning is not None: + this_ab = this_ab.where(AtBat.error == error) + + return_data = {'atbats': []} + for x in this_ab: + return_data['atbats'].append(model_to_dict(x)) + db.close() + return return_data + + +def get_one_atbat(game_id=None, batter_id=None, pitcher_id=None, on_base_code=None, inning=None, error=None): + all_abs = get_atbat(game_id, batter_id, pitcher_id, on_base_code, inning, error) + if len(all_abs['atbats']) == 0: + raise KeyError('At Bat not found') + else: + return all_abs['atbats'][len(all_abs['atbats']) - 1] + + +def get_atbat_totals(lineup_member_id): + this_member = LineupMember.get_by_id(lineup_member_id) + all_abs = AtBat.select( + fn.COUNT(AtBat.ab).alias('pas'), + fn.SUM(AtBat.ab).alias('abs'), + fn.SUM(AtBat.run).alias('runs'), + fn.SUM(AtBat.hit).alias('hits'), + fn.SUM(AtBat.rbi).alias('rbis'), + fn.SUM(AtBat.double).alias('doubles'), + fn.SUM(AtBat.triple).alias('triples'), + fn.SUM(AtBat.homerun).alias('hrs'), + fn.SUM(AtBat.bb).alias('bbs'), + fn.SUM(AtBat.so).alias('sos'), + fn.SUM(AtBat.hbp).alias('hbps'), + fn.SUM(AtBat.sac).alias('sacs'), + fn.SUM(AtBat.ibb).alias('ibbs'), + fn.SUM(AtBat.gidp).alias('gidps'), + fn.SUM(AtBat.bphr).alias('bphrs'), + fn.SUM(AtBat.bpfo).alias('bpfos'), + fn.SUM(AtBat.bp1b).alias('bp1bs'), + fn.SUM(AtBat.bplo).alias('bplos'), + ).where((AtBat.batter_id == this_member.player_id) & (AtBat.game == this_member.game) & (AtBat.pa > 0)) + + if all_abs.count() > 0: + this_line = { + 'empty': False, + 'pa': all_abs[0].pas if all_abs[0].pas else '', + 'ab': all_abs[0].abs if all_abs[0].abs else '', + 'run': all_abs[0].runs if all_abs[0].runs else '', + 'hit': all_abs[0].hits if all_abs[0].hits else '', + 'rbi': all_abs[0].rbis if all_abs[0].rbis else '', + 'double': all_abs[0].doubles if all_abs[0].doubles else '', + 'triple': all_abs[0].triples if all_abs[0].triples else '', + 'homerun': all_abs[0].hrs if all_abs[0].hrs else '', + 'bb': all_abs[0].bbs if all_abs[0].bbs else '', + 'so': all_abs[0].sos if all_abs[0].sos else '', + 'hbp': all_abs[0].hbps if all_abs[0].hbps else '', + 'sac': all_abs[0].sacs if all_abs[0].sacs else '', + 'ibb': all_abs[0].ibbs if all_abs[0].ibbs else '', + 'gidp': all_abs[0].gidps if all_abs[0].gidps else '', + 'bphr': all_abs[0].bphrs if all_abs[0].bphrs else '', + 'bpfo': all_abs[0].bpfos if all_abs[0].bpfos else '', + 'bp1b': all_abs[0].bp1bs if all_abs[0].bp1bs else '', + 'bplo': all_abs[0].bplos if all_abs[0].bplos else '' + } + else: + this_line = { + 'empty': True, + 'pa': '', + 'ab': '', + 'run': '', + 'hit': '', + 'rbi': '', + 'double': '', + 'triple': '', + 'homerun': '', + 'bb': '', + 'so': '', + 'hbp': '', + 'sac': '', + 'ibb': '', + 'gidp': '', + 'bphr': '', + 'bpfo': '', + 'bp1b': '', + 'bplo': '' + } + + return this_line + + +def get_atbat_pitching_totals(pitcher_member_id): + this_member = LineupMember.get_by_id(pitcher_member_id) + totals = AtBat.select( + fn.SUM(AtBat.hit).alias('hits'), + fn.SUM(AtBat.run).alias('runs'), + fn.SUM(AtBat.so).alias('sos'), + fn.SUM(AtBat.bb).alias('bbs'), + fn.SUM(AtBat.ibb).alias('ibbs'), + fn.SUM(AtBat.hbp).alias('hbps'), + fn.SUM(AtBat.homerun).alias('hrs'), + fn.SUM(AtBat.gidp).alias('gidp'), + ).where( + (AtBat.pitcher_id == this_member.player_id) & (AtBat.game == this_member.game) + ) + + earned_runs = AtBat.select( + fn.Sum(AtBat.run).alias('eruns') + ).where( + (AtBat.pitcher_id == this_member.player_id) & (AtBat.game == this_member.game) & (AtBat.error == 0) + ) + + outs_recorded = AtBat.select().where( + (AtBat.pitcher_id == this_member.player_id) & (AtBat.game == this_member.game) & (AtBat.hit == 0) & + (AtBat.bb == 0) & (AtBat.hbp == 0) & (AtBat.error == 0) + ) + + if outs_recorded.count() > 0: + complete_inn = math.floor((outs_recorded.count() + totals[0].gidp) / 3) + extra_outs = (outs_recorded.count() + totals[0].gidp) % 3 + logging.info(f'comp_in: {complete_inn} / extra_outs: {extra_outs}') + ip = complete_inn + (extra_outs / 3.0) + else: + ip = 0.0 + + if totals.count() > 0: + walks = totals[0].bbs + totals[0].ibbs + logging.info(f'bbs: {totals[0].bbs} / ibbs: {totals[0].ibbs} / walks: {walks}') + this_line = { + 'empty': False, + 'ip': ip, + 'hit': totals[0].hits if totals[0].hits else '', + 'run': totals[0].runs if totals[0].runs else '', + 'erun': earned_runs[0].eruns if earned_runs[0].eruns else '', + 'so': totals[0].sos if totals[0].sos else '', + 'bb': walks if walks else '', + 'hbp': totals[0].hbps if totals[0].hbps else '', + 'hr': totals[0].hrs if totals[0].hrs else '', + } + else: + this_line = { + 'empty': True, + 'ip': '', + 'hit': '', + 'run': '', + 'erun': '', + 'so': '', + 'bb': '', + 'hbp': '', + 'hr': '', + } + + return this_line + + +def patch_atbat(atbat_id, game_id=None, batter_id=None, pitcher_id=None, on_base_code=None, pa=None, ab=None, run=None, + hit=None, rbi=None, double=None, triple=None, homerun=None, bb=None, so=None, hbp=None, sac=None, + ibb=None, gidp=None, bphr=None, bpfo=None, bp1b=None, bplo=None, inning=None, error=None): + this_ab = AtBat.get_by_id(atbat_id) + if game_id is not None: + this_ab.game_id = game_id + if batter_id is not None: + this_ab.batter_id = batter_id + if pitcher_id is not None: + this_ab.pitcher_id = pitcher_id + if on_base_code is not None: + this_ab.on_base_code = on_base_code + if pa is not None: + this_ab.pa = pa + if ab is not None: + this_ab.ab = ab + if run is not None: + this_ab.run = run + if hit is not None: + this_ab.hit = hit + if rbi is not None: + this_ab.rbi = rbi + if double is not None: + this_ab.double = double + if triple is not None: + this_ab.triple = triple + if homerun is not None: + this_ab.homerun = homerun + if bb is not None: + this_ab.bb = bb + if so is not None: + this_ab.so = so + if hbp is not None: + this_ab.hbp = hbp + if sac is not None: + this_ab.sac = sac + if ibb is not None: + this_ab.ibb = ibb + if gidp is not None: + this_ab.gidp = gidp + if bphr is not None: + this_ab.bphr = bphr + if bpfo is not None: + this_ab.bpfo = bpfo + if bp1b is not None: + this_ab.bp1b = bp1b + if bplo is not None: + this_ab.bplo = bplo + if inning is not None: + this_ab.inning = inning + if error is not None: + this_ab.error = error + this_ab.save() + return_ab = model_to_dict(this_ab) + db.close() + return return_ab + + +class Running(BaseModel): + game = ForeignKeyField(Game) + runner_id = IntegerField() + stolen_base = IntegerField() + caught_stealing = IntegerField() + extra_base_attempt = IntegerField() + extra_base_taken = IntegerField() + + +class RunningModel(pydantic.BaseModel): + game_id: int + runner_id: int + stolen_base: Optional[int] = 0 + caught_stealing: Optional[int] = 0 + extra_base_attempt: Optional[int] = 0 + extra_base_taken: Optional[int] = 0 + + +def post_running(running_dict: dict): + running_model = RunningModel.parse_obj(running_dict) + new_running = Running( + game=Game.get_by_id(running_model.game_id), + runner_id=running_model.runner_id, + stolen_base=running_model.stolen_base, + caught_stealing=running_model.caught_stealing, + extra_base_attempt=running_model.extra_base_attempt, + extra_base_taken=running_model.extra_base_taken, + ) + new_running.save() + return_running = model_to_dict(new_running) + db.close() + return return_running + + +def get_running( + game_id=None, runner_id=None, stolen_base=None, caught_stealing=None, extra_base_attempt=None, + extra_base_taken=None): + this_running = Running.select() + if game_id: + this_running = this_running.where(Running.game == Game.get_by_id(game_id)) + if runner_id: + this_running = this_running.where(Running.runner_id == runner_id) + if stolen_base: + this_running = this_running.where(Running.stolen_base == stolen_base) + if caught_stealing: + this_running = this_running.where(Running.caught_stealing == caught_stealing) + if extra_base_attempt: + this_running = this_running.where(Running.extra_base_attempt == extra_base_attempt) + if extra_base_taken: + this_running = this_running.where(Running.extra_base_taken == extra_base_taken) + + return_data = {'running': []} + for x in this_running: + return_data['running'].append(model_to_dict(x)) + db.close() + return return_data + + +def get_running_totals(lineup_member_id): + this_member = LineupMember.get_by_id(lineup_member_id) + all_running = Running.select( + fn.SUM(Running.stolen_base).alias('sb'), + fn.SUM(Running.caught_stealing).alias('cs'), + fn.SUM(Running.extra_base_attempt).alias('xba'), + fn.SUM(Running.extra_base_taken).alias('xbt'), + ).where( + (Running.runner_id == this_member.player_id) & (Running.game == this_member.game) + ) + + if all_running.count() > 0: + this_line = { + 'empty': False, + 'sb': all_running[0].sb if all_running[0].sb else '', + 'cs': all_running[0].cs if all_running[0].cs else '', + 'xba': all_running[0].xba if all_running[0].xba else '', + 'xbt': all_running[0].xbt if all_running[0].xbt else '', + } + else: + this_line = { + 'empty': True, + 'sb': '', + 'cs': '', + 'xba': '', + 'xbt': '', + } + + return this_line + + +def patch_running( + running_id, game_id=None, runner_id=None, stolen_base=None, caught_stealing=None, extra_base_attempt=None, + extra_base_taken=None): + this_running = Running.get_by_id(running_id) + if game_id is not None: + this_running.game = Game.get_by_id(game_id) + if runner_id is not None: + this_running.runner_id = runner_id + if stolen_base is not None: + this_running.stolen_base = stolen_base + if caught_stealing is not None: + this_running.caught_stealing = caught_stealing + if extra_base_attempt is not None: + this_running.extra_base_attempt = extra_base_attempt + if extra_base_taken is not None: + this_running.extra_base_taken = extra_base_taken + this_running.save() + return_running = model_to_dict(this_running) + db.close() + return return_running + + +class Defense(BaseModel): + game = ForeignKeyField(Game) + at_bat = ForeignKeyField(AtBat, null=True) + player_id = IntegerField() + x_check = BooleanField() + position = CharField() + hit = BooleanField() + error = BooleanField() + stolen_base_attempt = BooleanField() + caught_stealing = BooleanField() + rob_attempt = BooleanField() + rob_success = BooleanField() + runner_adv_att = BooleanField() + runner_throw_out = BooleanField() + + +class DefenseModel(pydantic.BaseModel): + game_id: int + at_bat_id: Optional[int] = 0 + player_id: int + x_check: Optional[int] = 0 + position: str + hit: Optional[int] = 0 + error: Optional[int] = 0 + stolen_base_attempt: Optional[int] = 0 + caught_stealing: Optional[int] = 0 + rob_attempt: Optional[int] = 0 + rob_success: Optional[int] = 0 + runner_adv_att: Optional[int] = 0 + runner_throw_out: Optional[int] = 0 + + +def post_defense(defense_dict: dict): + defense_model = DefenseModel.parse_obj(defense_dict) + new_defense = Defense( + game=Game.get_by_id(defense_model.game_id), + at_bat=AtBat.get_by_id(defense_model.at_bat_id) if defense_model.at_bat_id else None, + player_id=defense_model.player_id, + x_check=defense_model.x_check, + position=defense_model.position, + hit=defense_model.hit, + error=defense_model.error, + stolen_base_attempt=defense_model.stolen_base_attempt, + caught_stealing=defense_model.caught_stealing, + rob_attempt=defense_model.rob_attempt, + rob_success=defense_model.rob_success, + runner_adv_att=defense_model.runner_adv_att, + runner_throw_out=defense_model.runner_throw_out, + ) + new_defense.save() + return_defense = model_to_dict(new_defense) + db.close() + return return_defense + + +def get_defense( + game_id=None, at_bat_id=None, player_id=None, x_check=None, position=None, hit=None, error=None, sba=None, + cs=None, roba=None, robs=None, raa=None, rto=None): + this_defense = Defense.select() + if game_id: + this_defense = this_defense.where(Defense.game == Game.get_by_id(game_id)) + if at_bat_id: + this_defense = this_defense.where(Defense.at_bat == AtBat.get_by_id(at_bat_id)) + if player_id: + this_defense = this_defense.where(Defense.player_id == player_id) + if x_check: + this_defense = this_defense.where(Defense.x_check == x_check) + if position: + this_defense = this_defense.where(Defense.position == position) + if hit: + this_defense = this_defense.where(Defense.hit == hit) + if error: + this_defense = this_defense.where(Defense.error == error) + if sba: + this_defense = this_defense.where(Defense.sba == sba) + if cs: + this_defense = this_defense.where(Defense.cs == cs) + if roba: + this_defense = this_defense.where(Defense.roba == roba) + if robs: + this_defense = this_defense.where(Defense.robs == robs) + if raa: + this_defense = this_defense.where(Defense.raa == raa) + if rto: + this_defense = this_defense.where(Defense.rto == rto) + + return_data = {'defense': []} + for x in this_defense: + return_data['defense'].append(model_to_dict(x)) + db.close() + return return_data + + +def get_defense_totals(lineup_member_id): + this_member = LineupMember.get_by_id(lineup_member_id) + all_pos = Defense.select(Defense.position).where( + (Defense.player_id == this_member.player_id) & (Defense.game == this_member.game) + ) + + all_lines = [] + + for x in all_pos: + all_defense = Defense.select( + fn.SUM(Defense.x_check).alias('xch'), + fn.SUM(Defense.hit).alias('xhit'), + fn.SUM(Defense.error).alias('errors'), + fn.SUM(Defense.stolen_base_attempt).alias('sba'), + fn.SUM(Defense.caught_stealing).alias('csc'), + fn.SUM(Defense.rob_attempt).alias('roba'), + fn.SUM(Defense.rob_success).alias('robs'), + fn.SUM(Defense.runner_adv_att).alias('raa'), + fn.SUM(Defense.runner_throw_out).alias('rto'), + ).where( + (Defense.player_id == this_member.player_id) & (Defense.game == this_member.game) & + (Defense.position == x.position) + ) + + all_lines.append({ + 'pos': x.position, + 'xch': all_defense[0].xch if all_defense[0].xch else '', + 'xhit': all_defense[0].xhit if all_defense[0].xhit else '', + 'errors': all_defense[0].errors if all_defense[0].errors else '', + 'sba': all_defense[0].sba if all_defense[0].sba else '', + 'csc': all_defense[0].csc if all_defense[0].csc else '', + 'roba': all_defense[0].roba if all_defense[0].roba else '', + 'robs': all_defense[0].robs if all_defense[0].robs else '', + 'raa': all_defense[0].raa if all_defense[0].raa else '', + 'rto': all_defense[0].rto if all_defense[0].rto else '', + }) + + db.close() + return {'defense': all_lines} + + +def patch_defense( + defense_id, game_id=None, at_bat_id=None, player_id=None, x_check=None, position=None, hit=None, error=None, + sba=None, cs=None, roba=None, robs=None, raa=None, rto=None): + this_defense = Defense.get_by_id(defense_id) + if game_id is not None: + this_defense.game = Game.get_by_id(game_id) + if at_bat_id is not None: + this_defense.at_bat = AtBat.get_by_id(at_bat_id) + if player_id is not None: + this_defense.x_check = x_check + if position is not None: + this_defense.position = position + if hit is not None: + this_defense.hit = hit + if error is not None: + this_defense.error = error + if sba is not None: + this_defense.sba = sba + if cs is not None: + this_defense.cs = cs + if roba is not None: + this_defense.roba = roba + if robs is not None: + this_defense.robs = robs + if raa is not None: + this_defense.raa = raa + if rto is not None: + this_defense.rto = rto + this_defense.save() + return_defense = model_to_dict(this_defense) + db.close() + return return_defense + + +class Chaos(BaseModel): + game = ForeignKeyField(Game) + pitcher_id = IntegerField() + catcher_id = IntegerField() + wild_pitch = IntegerField() + passed_ball = IntegerField() + pick_off = IntegerField() + balk = IntegerField() + + +class ChaosModel(pydantic.BaseModel): + game_id: int + pitcher_id: int + catcher_id: int + wild_pitch: int = 0 + passed_ball: int = 0 + pick_off: int = 0 + balk: int = 0 + + +def post_chaos(chaos_dict: dict): + chaos_model = ChaosModel.parse_obj(chaos_dict) + new_chaos = Chaos( + game=Game.get_by_id(chaos_model.game_id), + pitcher_id=chaos_model.pitcher_id, + catcher_id=chaos_model.catcher_id, + wild_pitch=chaos_model.wild_pitch, + passed_ball=chaos_model.passed_ball, + pick_off=chaos_model.pick_off, + balk=chaos_model.balk, + ) + new_chaos.save() + return_chaos = model_to_dict(new_chaos) + db.close() + return return_chaos + + +def get_chaos( + game_id=None, pitcher_id=None, catcher_id=None, wild_pitch=None, passed_ball=None, pick_off=None, balk=None): + this_chaos = Chaos.select() + if game_id: + this_chaos = this_chaos.where(Chaos.game == Game.get_by_id(game_id)) + if pitcher_id: + this_chaos = this_chaos.where(Chaos.pitcher_id == pitcher_id) + if catcher_id: + this_chaos = this_chaos.where(Chaos.catcher_id == catcher_id) + if wild_pitch: + this_chaos = this_chaos.where(Chaos.wild_pitch == wild_pitch) + if passed_ball: + this_chaos = this_chaos.where(Chaos.passed_ball == passed_ball) + if pick_off: + this_chaos = this_chaos.where(Chaos.pick_off == pick_off) + if balk: + this_chaos = this_chaos.where(Chaos.balk == balk) + + return_data = {'chaos': []} + for x in this_chaos: + return_data['chaos'].append(model_to_dict(x)) + db.close() + return return_data + + +def get_chaos_totals(lineup_member_id): + this_member = LineupMember.get_by_id(lineup_member_id) + all_chaos = Chaos.select( + fn.SUM(Chaos.wild_pitch).alias('wp'), + fn.SUM(Chaos.passed_ball).alias('pb'), + fn.SUM(Chaos.pick_off).alias('po'), + fn.SUM(Chaos.balk).alias('bk'), + ).where( + ((Chaos.pitcher_id == this_member.player_id) | (Chaos.catcher_id == this_member.player_id)) & + (Chaos.game == this_member.game) + ) + + if all_chaos.count() > 0: + this_line = { + 'empty': False, + 'wp': all_chaos[0].wp if all_chaos[0].wp else '', + 'pb': all_chaos[0].pb if all_chaos[0].pb else '', + 'po': all_chaos[0].po if all_chaos[0].po else '', + 'bk': all_chaos[0].bk if all_chaos[0].bk else '', + } + else: + this_line = { + 'empty': True, + 'wp': '', + 'pb': '', + 'po': '', + 'bk': '', + } + + return this_line + + +def patch_chaos( + chaos_id, pitcher_id=None, catcher_id=None, wild_pitch=None, passed_ball=None, pick_off=None, balk=None): + this_chaos = Chaos.get_by_id(chaos_id) + if pitcher_id is not None: + this_chaos.pitcher_id = pitcher_id + if catcher_id is not None: + this_chaos.catcher_id = catcher_id + if wild_pitch is not None: + this_chaos.wild_pitch = wild_pitch + if passed_ball is not None: + this_chaos.passed_ball = passed_ball + if pick_off is not None: + this_chaos.pick_off = pick_off + if balk is not None: + this_chaos.balk = balk + this_chaos.save() + return_chaos = model_to_dict(this_chaos) + db.close() + return return_chaos + + +class LineupMember(BaseModel): + game = ForeignKeyField(Game) + team_id = IntegerField() + player_id = IntegerField() + position = CharField() + batting_order = IntegerField() + active = BooleanField(default=True) + + +class LineupMemberModel(pydantic.BaseModel): + game_id: int + team_id: int + player_id: int + position: str + batting_order: int + active: Optional[bool] = True + + +def post_lineup_member(member_dict: dict): + lineup_model = LineupMemberModel.parse_obj(member_dict) + new_lineup = LineupMember( + game=Game.get_by_id(lineup_model.game_id), + team_id=lineup_model.team_id, + player_id=lineup_model.player_id, + position=lineup_model.position, + batting_order=lineup_model.batting_order, + active=lineup_model.active, + ) + new_lineup.save() + return_lineup = model_to_dict(new_lineup) + db.close() + return return_lineup + + +def get_lineup_members(game_id=None, team_id=None, player_id=None, position=None, batting_order=None, active=None): + this_member = LineupMember.select() + if game_id is not None: + this_game = Game.get_by_id(game_id) + this_member = this_member.where(LineupMember.game == this_game) + if team_id is not None: + this_member = this_member.where(LineupMember.team_id == team_id) + if player_id is not None: + this_member = this_member.where(LineupMember.player_id == player_id) + if position is not None: + this_member = this_member.where(LineupMember.position == position) + if batting_order is not None: + this_member = this_member.where(LineupMember.batting_order == batting_order) + if active is not None: + this_member = this_member.where(LineupMember.active == active) + + return_data = {'members': []} + for x in this_member: + return_data['members'].append(model_to_dict(x)) + db.close() + return return_data + + +def get_one_member(game_id=None, team_id=None, player_id=None, position=None, batting_order=None, active=None): + all_members = get_lineup_members(game_id, team_id, player_id, position, batting_order, active) + if len(all_members['members']) == 0: + raise KeyError('Lineup member not found') + else: + return all_members['members'][len(all_members['members']) - 1] + + +def patch_lineup_member( + member_id, game_id=None, team_id=None, player_id=None, position=None, batting_order=None, active=None): + this_member = LineupMember.get_by_id(member_id) + if game_id is not None: + this_member.game_id = game_id + if team_id is not None: + this_member.team_id = team_id + if player_id is not None: + this_member.player_id = player_id + if position is not None: + this_member.position = position + if batting_order is not None: + this_member.batting_order = batting_order + if active is not None: + this_member.active = active + this_member.save() + return_member = model_to_dict(this_member) + db.close() + return return_member + + +class GameState(BaseModel): + game = ForeignKeyField(Game) + top_half = BooleanField(default=True) + inning = IntegerField(default=1) + outs = IntegerField(default=0) + away_batter_up = IntegerField(default=1) + home_batter_up = IntegerField(default=1) + away_score = IntegerField(default=0) + home_score = IntegerField(default=0) + on_first = ForeignKeyField(LineupMember, null=True) + on_second = ForeignKeyField(LineupMember, null=True) + on_third = ForeignKeyField(LineupMember, null=True) + final = BooleanField(default=False) + + +class GameStateModel(pydantic.BaseModel): + game_id: int + top_half: Optional[bool] = True + inning: Optional[int] = 1 + outs: Optional[int] = 0 + away_batter_up: Optional[int] = 1 + home_batter_up: Optional[int] = 1 + away_score: Optional[int] = 0 + home_score: Optional[int] = 0 + on_first_id: Optional[int] = None + on_second_id: Optional[int] = None + on_third_id: Optional[int] = None + final: Optional[bool] = False + + +def post_game_state(state_dict: dict): + state_model = GameStateModel.parse_obj(state_dict) + if state_model.on_first_id: + on_first = LineupMember.get_by_id(state_model.on_first_id) + else: + on_first = None + if state_model.on_second_id: + on_second = LineupMember.get_by_id(state_model.on_second_id) + else: + on_second = None + if state_model.on_second_id: + on_third = LineupMember.get_by_id(state_model.on_third_id) + else: + on_third = None + new_state = GameState( + game=Game.get_by_id(state_model.game_id), + top_half=state_model.top_half, + inning=state_model.inning, + outs=state_model.outs, + away_batter_up=state_model.away_batter_up, + home_batter_up=state_model.home_batter_up, + away_score=state_model.away_score, + home_score=state_model.home_score, + on_first=on_first, + on_second=on_second, + on_third=on_third + ) + new_state.save() + return_state = new_state + db.close() + return return_state + + +def get_game_state(game_id=None, top_half=None, inning=None, outs=None, away_batter_up=None, home_batter_up=None, + away_score=None, home_score=None, final=None): + this_state = GameState.select() + if game_id is not None: + this_game = Game.get_by_id(game_id) + this_state = this_state.where(GameState.game == this_game) + if top_half is not None: + this_state = this_state.where(GameState.top_half == top_half) + if inning is not None: + this_state = this_state.where(GameState.inning == inning) + if outs is not None: + this_state = this_state.where(GameState.outs == outs) + if away_batter_up is not None: + this_state = this_state.where(GameState.away_batter_up == away_batter_up) + if home_batter_up is not None: + this_state = this_state.where(GameState.home_batter_up == home_batter_up) + if away_score is not None: + this_state = this_state.where(GameState.away_score == away_score) + if home_score is not None: + this_state = this_state.where(GameState.home_score == home_score) + if final is not None: + this_state = this_state.where(GameState.final == final) + + return_data = {'states': []} + for x in this_state: + return_data['states'].append(model_to_dict(x)) + db.close() + return return_data + + +def get_one_game_state(game_id=None, top_half=None, inning=None, outs=None, away_batter_up=None, home_batter_up=None, + away_score=None, home_score=None, final=None): + all_states = get_game_state( + game_id, top_half, inning, outs, away_batter_up, home_batter_up, away_score, home_score, final + ) + if len(all_states['states']) == 0: + raise KeyError('Game State not found') + else: + return all_states['states'][(len(all_states['states'])) - 1] + + +def patch_game_state( + state_id, top_half=None, inning=None, outs=None, away_batter_up=None, home_batter_up=None, away_score=None, + home_score=None, final=None, on_first_id=None, on_second_id=None, on_third_id=None): + this_state = GameState.get_by_id(state_id) + if top_half is not None: + this_state.top_half = top_half + if inning is not None: + this_state.inning = inning + if outs is not None: + this_state.outs = outs + if away_batter_up is not None: + this_state.away_batter_up = away_batter_up + if home_batter_up is not None: + this_state.home_batter_up = home_batter_up + if away_score is not None: + this_state.away_score = away_score + if home_score is not None: + this_state.home_score = home_score + if final is not None: + this_state.final = final + if on_first_id is not None: + if on_first_id: + on_first = LineupMember.get_by_id(on_first_id) + else: + on_first = None + this_state.on_first = on_first + if on_second_id is not None: + if on_second_id: + on_second = LineupMember.get_by_id(on_second_id) + else: + on_second = None + this_state.on_second = on_second + if on_third_id is not None: + if on_third_id: + on_third = LineupMember.get_by_id(on_third_id) + else: + on_third = None + this_state.on_third = on_third + this_state.save() + return_state = model_to_dict(this_state) + db.close() + return return_state + + +db.create_tables([Game, AtBat, Running, Defense, LineupMember, GameState, Chaos, Defense, Running]) +db.close() diff --git a/db_calls_gameplay.py b/db_calls_gameplay.py new file mode 100644 index 0000000..6e3a405 --- /dev/null +++ b/db_calls_gameplay.py @@ -0,0 +1,1196 @@ +import logging +import pydantic +import requests + +from typing import Optional +from peewee import * +from playhouse.shortcuts import model_to_dict +from dataclasses import dataclass + +from helpers import SBA_SEASON, get_player_url +from db_calls import get_team_by_owner, get_one_team, get_one_player + +db = SqliteDatabase( + 'storage/gameplay.db', + pragmas={ + 'journal_mode': 'wal', + 'cache_size': -1 * 64000, + 'synchronous': 0 + } +) +PD_DB_URL = 'http://pd-database/api' +PD_SEASON = 4 + + +def param_char(other_params): + if other_params: + return '&' + else: + return '?' + + +def pd_get_req_url(endpoint: str, api_ver: int = 1, object_id: int = None, params: list = None): + req_url = f'{PD_DB_URL}/v{api_ver}/{endpoint}{"/" if object_id is not None else ""}' \ + f'{object_id if object_id is not None else ""}' + + if params: + other_params = False + for x in params: + req_url += f'{param_char(other_params)}{x[0]}={x[1]}' + other_params = True + logging.info(f'PD GET: {req_url}') + + return req_url + + +def pd_db_get(endpoint: str, api_ver: int = 1, object_id: int = None, params: list = None, none_okay: bool = True): + req_url = pd_get_req_url(endpoint, api_ver=api_ver, object_id=object_id, params=params) + logging.info(f'get:\n{endpoint} id: {object_id} params: {params}') + + resp = requests.get(req_url, timeout=3) + if resp.status_code == 200: + data = resp.json() + logging.info(f'return: {data}') + return data + elif none_okay: + data = resp.json() + logging.info(f'return: {data}') + return None + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +def pd_get_one_team(team_abbrev: str): + team = pd_db_get('teams', params=[('abbrev', team_abbrev), ('season', PD_SEASON)], none_okay=False)['teams'][0] + return team + + # req_url = pd_get_req_url('teams', ) + # + # resp = requests.get(req_url, timeout=3) + # if resp.status_code == 200: + # data = resp.json() + # logging.info(f'return: {data}') + # return data['teams'][0] + # else: + # logging.warning(resp.text) + # raise ValueError(f'PD DB: {resp.text}') + + +def pd_get_card_by_id(card_id: int): + return pd_db_get('cards', object_id=card_id, none_okay=False) + + +class BaseModel(Model): + class Meta: + database = db + + +class ManagerAi(BaseModel): + name = CharField(unique=True) + + +@dataclass +class StratManagerAi: + name: str + + +db.create_tables([ManagerAi]) + + +class Game(BaseModel): + away_team_id = IntegerField() + home_team_id = IntegerField() + week_num = IntegerField(null=True) + game_num = IntegerField(null=True) + channel_id = IntegerField() + active = BooleanField(default=True) + is_pd = BooleanField(default=False) + opp_ai = ForeignKeyField(ManagerAi, null=True) + + # TODO: add get_away_team and get_home_team that deals with SBa/PD and returns Team object + + +@dataclass +class StratGame: + id: int + away_team_id: int + home_team_id: int + channel_id: int + week_num: int = None + game_num: int = None + active: bool = True + is_pd: bool = False + opp_ai: StratManagerAi = None + + +db.create_tables([Game]) + + +# class GameModel(pydantic.BaseModel): +# away_team_id: int +# home_team_id: int +# week_num: Optional[int] = None +# game_num: Optional[int] = None +# channel_id: int +# active: Optional[bool] = True +# is_pd: Optional[bool] = True + + +def post_game(game_dict: dict): + # game_model = GameModel.parse_obj(game_dict) + # new_game = Game( + # away_team_id=game_model.away_team_id, + # home_team_id=game_model.home_team_id, + # week_num=game_model.week_num, + # game_num=game_model.game_num, + # channel_id=game_model.channel_id, + # active=game_model.active, + # is_pd=game_model.is_pd + # ) + # new_game.save() + new_game = Game.create(**game_dict) + # return_game = model_to_dict(new_game) + return_game = StratGame(new_game.id, new_game.away_team_id, new_game.home_team_id, new_game.channel_id, + new_game.week_num, new_game.game_num, new_game.active, new_game.is_pd, new_game.opp_ai.id) + db.close() + + return return_game + + +def get_one_game(away_team_id=None, home_team_id=None, week_num=None, game_num=None, channel_id=None, active=None) \ + -> Optional[StratGame]: + this_game = Game.select() + if away_team_id is not None: + this_game = this_game.where(Game.away_team_id == away_team_id) + if home_team_id is not None: + this_game = this_game.where(Game.home_team_id == home_team_id) + if week_num is not None: + this_game = this_game.where(Game.week_num == week_num) + if game_num is not None: + this_game = this_game.where(Game.game_num == game_num) + if channel_id is not None: + this_game = this_game.where(Game.channel_id == channel_id) + if active is not None: + this_game = this_game.where(Game.active == active) + + if this_game.count() > 0: + # return_game = model_to_dict(this_game[0]) + r_game = this_game[0] + return_game = StratGame(r_game.id, r_game.away_team_id, r_game.home_team_id, r_game.channel_id, + r_game.week_num, r_game.game_num, r_game.active, r_game.is_pd, + r_game.opp_ai.id if r_game.opp_ai else None) + db.close() + return return_game + else: + return None + + +async def get_game_team(game: StratGame, gm_id: int = None, team_abbrev: str = None, team_id: int = None) -> 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.info(f'getting game team for game {game.id} / gm_id: {gm_id} / ' + f'tm_abbrev: {team_abbrev} / team_id: {team_id}') + if game.is_pd: + if gm_id: + return pd_db_get('teams', params=[('season', PD_SEASON), ('gm_id', gm_id)])['teams'][0] + elif team_id: + return pd_db_get('teams', object_id=team_id) + else: + return pd_db_get('teams', params=[('season', PD_SEASON), ('abbrev', team_abbrev)])['teams'][0] + else: + if gm_id: + return await get_team_by_owner(season=SBA_SEASON, owner_id=gm_id) + elif team_id: + return await get_one_team(team_id) + else: + return await get_one_team(team_abbrev, season=SBA_SEASON) + + +def patch_game( + game_id, away_team_id=None, home_team_id=None, week_num=None, game_num=None, channel_id=None, active=None): + this_game = Game.get_by_id(game_id) + if away_team_id is not None: + this_game.away_team_id = away_team_id + if home_team_id is not None: + this_game.home_team_id = home_team_id + if week_num is not None: + this_game.week_num = week_num + if game_num is not None: + this_game.game_num = game_num + if channel_id is not None: + this_game.channel_id = channel_id + if active is not None: + this_game.active = active + 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.week_num, this_game.game_num, this_game.active, this_game.is_pd, + this_game.opp_ai.id) + db.close() + return return_game + + +class Lineup(BaseModel): + game = ForeignKeyField(Game) + team_id = IntegerField() + player_id = IntegerField() + card_id = IntegerField(null=True) # Only needed for Paper Dynasty games + position = CharField() + batting_order = IntegerField() + after_play = IntegerField() + active = BooleanField(default=True) + + +@dataclass +class StratLineup: + id: int + game: StratGame + team_id: int + player_id: int + position: str + batting_order: int + after_play: int + active: bool = True + card_id: int = None + + +def convert_stratlineup(lineup: Lineup) -> StratLineup: + lineup_dict = model_to_dict(lineup) + lineup_dict['game'] = StratGame(**lineup_dict['game']) + return StratLineup(**lineup_dict) + + +db.create_tables([Lineup]) + + +def get_one_lineup( + game_id: int, lineup_id: int = None, team_id: int = None, batting_order: int = None, position: str = None, + active: bool = True, as_obj: bool = False) -> StratLineup: + if not batting_order and not position and not lineup_id: + raise KeyError(f'One of batting_order, position, or lineup_id must not be None') + + if lineup_id: + this_lineup = Lineup.get_by_id(lineup_id) + + elif batting_order: + this_lineup = Lineup.get_or_none( + Lineup.game_id == game_id, Lineup.team_id == team_id, Lineup.batting_order == batting_order, + Lineup.active == active + ) + else: + this_lineup = Lineup.get_or_none( + Lineup.game_id == game_id, Lineup.team_id == team_id, Lineup.position == position, + Lineup.active == active + ) + logging.info(f'get_one_lineup / this_lineup: {this_lineup}') + + if as_obj: + return this_lineup + if not this_lineup: + return None + + # return_value = model_to_dict(this_lineup) + # return_value = StratLineup(**vars(this_lineup)["__data__"]) + return_value = convert_stratlineup(this_lineup) + + # return_value = StratLineup( + # game=StratGame(**model_to_dict(this_lineup.game)), + # team_id=this_lineup.team_id, + # player_id=this_lineup.player_id, + # position=this_lineup.position, + # batting_order=this_lineup.batting_order, + # after_play=this_lineup.after_play, + # active= + # ) + db.close() + return return_value + + +async def get_team_lineups(game_id: int, team_id: int, as_string: bool = True): + all_lineups = Lineup.select().where( + (Lineup.game_id == game_id) & (Lineup.team_id == team_id) & (Lineup.active == True) + ).order_by(Lineup.batting_order) + + if all_lineups.count() == 0: + return None + + if as_string: + l_string = '' + this_game = Game.get_by_id(game_id) + for x in all_lineups: + l_string += f'{x.batting_order}. {player_link(this_game, await get_player(this_game, x))} - {x.position}\n' + + db.close() + return l_string + + return_lineups = [] + for x in all_lineups: + # return_lineups.append(model_to_dict(x)) + return_lineups.append(convert_stratlineup(x)) + + db.close() + return return_lineups + + +def post_lineups(lineups: list): + with db.atomic(): + Lineup.insert_many(lineups).execute() + db.close() + + +def patch_lineup(lineup_id, active: Optional[bool] = None) -> StratLineup: + this_lineup = Lineup.get_by_id(lineup_id) + this_lineup.active = active + this_lineup.save() + + # return_lineup = model_to_dict(this_lineup) + return_value = convert_stratlineup(this_lineup) + db.close() + + return return_value + + +def make_sub(lineup_dict: dict): + # Check for dupe player / card + this_game = Game.get_by_id(lineup_dict['game_id']) + + if this_game.is_pd: + player_conflict = Lineup.select().where( + (Lineup.game_id == lineup_dict['game_id']) & (Lineup.team_id == lineup_dict['team_id']) & + (Lineup.card_id == lineup_dict['card_id']) + ) + db_error = f'This card is already in the lineup' + else: + player_conflict = Lineup.select().where( + (Lineup.game_id == lineup_dict['game_id']) & (Lineup.team_id == lineup_dict['team_id']) & + (Lineup.player_id == lineup_dict['player_id']) + ) + db_error = f'This player is already in the lineup' + + if player_conflict.count(): + db.close() + raise DatabaseError(db_error) + + subbed_player = Lineup.get_or_none( + Lineup.game_id == lineup_dict['game_id'], Lineup.team_id == lineup_dict['team_id'], + Lineup.batting_order == lineup_dict['batting_order'], Lineup.active == True + ) + logging.info(f'subbed_player: {subbed_player}') + + if subbed_player: + subbed_player = patch_lineup(subbed_player.id, active=False) + + new_lineup = Lineup.create(**lineup_dict) + # return_lineup = model_to_dict(new_lineup) + return_value = convert_stratlineup(new_lineup) + + curr_play = get_current_play(lineup_dict['game_id']) + + if curr_play.batter.id == subbed_player.id: + patch_play(curr_play.id, batter_id=return_value.id) + if curr_play.pitcher.id == subbed_player.id: + patch_play(curr_play.id, pitcher_id=return_value.id) + if curr_play.catcher is not None and curr_play.catcher.id == subbed_player.id: + patch_play(curr_play.id, catcher_id=return_value.id) + + db.close() + + return return_value + + +async def get_player(game, lineup_member) -> dict: + if isinstance(game, Game): + if game.is_pd: + this_card = pd_get_card_by_id(lineup_member.card_id) + player = this_card['player'] + player['name'] = player['p_name'] + player['team'] = this_card['team'] + return player + else: + return await get_one_player( + # lineup_member.player_id if isinstance(lineup_member, Lineup) else lineup_member['player_id'] + lineup_member.player_id + ) + elif isinstance(game, StratGame): + if game.is_pd: + # card_id = lineup_member.card_id if isinstance(lineup_member, Lineup) else lineup_member['card_id'] + card_id = lineup_member['card_id'] if isinstance(lineup_member, dict) else lineup_member.card_id + this_card = pd_get_card_by_id(card_id) + player = this_card['player'] + player['name'] = player['p_name'] + player['team'] = this_card['team'] + logging.info(f'player: {player}') + return player + else: + return await get_one_player( + # lineup_member.player_id if isinstance(lineup_member, Lineup) else lineup_member['player_id'] + lineup_member.player_id + ) + else: + raise TypeError(f'Cannot get player; game is not a valid object') + + +def player_link(game, player): + if isinstance(game, Game): + if game.is_pd: + return f'[{player["p_name"]}]({player["image"]})' + else: + return get_player_url(player) + elif isinstance(game, StratGame): + if game.is_pd: + return f'[{player["p_name"]}]({player["image"]})' + else: + return get_player_url(player) + else: + raise TypeError(f'Cannot get player link; game is not a valid object') + + +class Play(BaseModel): + game = ForeignKeyField(Game) + play_num = IntegerField() + batter = ForeignKeyField(Lineup) + pitcher = ForeignKeyField(Lineup, null=True) + on_base_code = IntegerField() + inning_half = CharField() + inning_num = IntegerField() + batting_order = IntegerField() + starting_outs = IntegerField() + away_score = IntegerField() + home_score = IntegerField() + on_first = ForeignKeyField(Lineup, null=True) + on_first_final = IntegerField(null=True) + on_second = ForeignKeyField(Lineup, null=True) + on_second_final = IntegerField(null=True) + on_third = ForeignKeyField(Lineup, null=True) + on_third_final = IntegerField(null=True) + batter_final = IntegerField(null=True) + pa = IntegerField(default=0) + ab = IntegerField(default=0) + # run = IntegerField(null=True) + hit = IntegerField(default=0) + rbi = IntegerField(default=0) + double = IntegerField(default=0) + triple = IntegerField(default=0) + homerun = IntegerField(default=0) + bb = IntegerField(default=0) + so = IntegerField(default=0) + hbp = IntegerField(default=0) + sac = IntegerField(default=0) + ibb = IntegerField(default=0) + gidp = IntegerField(default=0) + bphr = IntegerField(default=0) + bpfo = IntegerField(default=0) + bp1b = IntegerField(default=0) + bplo = IntegerField(default=0) + sb = IntegerField(default=0) + cs = IntegerField(default=0) + outs = IntegerField(default=0) + catcher = ForeignKeyField(Lineup, null=True) + defender = ForeignKeyField(Lineup, null=True) + runner = ForeignKeyField(Lineup, null=True) + check_pos = CharField(null=True) + error = IntegerField(default=0) + wild_pitch = IntegerField(default=0) + passed_ball = IntegerField(default=0) + pick_off = IntegerField(default=0) + balk = IntegerField(default=0) + complete = BooleanField(default=False) + locked = BooleanField(default=False) + + +@dataclass +class StratPlay: + id: int + game: StratGame + play_num: int + batter: StratLineup + pitcher: StratLineup + on_base_code: int + inning_half: str + inning_num: int + batting_order: int + starting_outs: int + away_score: int + home_score: int + on_first: StratLineup = None + on_first_final: int = None + on_second: StratLineup = None + on_second_final: int = None + on_third: StratLineup = None + on_third_final: int = None + batter_final: int = None + pa: int = 0 + ab: int = 0 + # run: int = 0 + hit: int = 0 + rbi: int = 0 + double: int = 0 + triple: int = 0 + homerun: int = 0 + bb: int = 0 + so: int = 0 + hbp: int = 0 + sac: int = 0 + ibb: int = 0 + gidp: int = 0 + bphr: int = 0 + bpfo: int = 0 + bp1b: int = 0 + bplo: int = 0 + sb: int = 0 + cs: int = 0 + outs: int = 0 + catcher: StratLineup = None + defender: StratLineup = None + runner: StratLineup = None + check_pos: str = None + error: int = 0 + wild_pitch: int = 0 + passed_ball: int = 0 + pick_off: int = 0 + balk: int = 0 + complete: bool = False + locked: bool = False + + +def convert_stratplay(play: Play) -> StratPlay: + play_dict = model_to_dict(play) + play_dict['game'] = StratGame(**play_dict['game']) + if play_dict['batter']: + play_dict['batter'] = convert_stratlineup(play.batter) + if play_dict['pitcher']: + play_dict['pitcher'] = convert_stratlineup(play.pitcher) + if play_dict['on_first']: + play_dict['on_first'] = convert_stratlineup(play.on_first) + if play_dict['on_second']: + play_dict['on_second'] = convert_stratlineup(play.on_second) + if play_dict['on_third']: + play_dict['on_third'] = convert_stratlineup(play.on_third) + if play_dict['catcher']: + play_dict['catcher'] = convert_stratlineup(play.catcher) + if play_dict['defender']: + play_dict['defender'] = convert_stratlineup(play.defender) + if play_dict['runner']: + play_dict['runner'] = convert_stratlineup(play.runner) + + return StratPlay(**play_dict) + + +db.create_tables([Play]) + + +def post_play(play_dict: dict) -> StratPlay: + logging.info(f'play_dict: {play_dict}') + new_play = Play.create(**play_dict) + # return_play = model_to_dict(new_play) + return_play = convert_stratplay(new_play) + db.close() + + return return_play + + +def patch_play( + play_id, batter_id: int = None, pitcher_id: int = None, catcher_id: int = None, locked: bool = None, + pa: int = None, ab: int = None, hit: int = None, double: int = None, triple: int = None, homerun: int = None, + outs: int = None, so: int = None, bp1b: int = None, bplo: int = None, bphr: int = None, bpfo: int = None, + walk: int = None, hbp: int = None, ibb: int = None, sac: int = None, sb: int = None, cs: int = None, + defender_id: int = None, check_pos: str = None, error: int = None, play_num: int = None, + on_first_id: int = None, on_first_final: int = None, on_second_id: int = None, on_second_final: int = None, + on_third_id: int = None, on_third_final: int = None, starting_outs: int = None, runner_id: int = None, + complete: bool = None, rbi: int = None, wp: int = None, pb: int = None, pick: int = None, balk: int = None): + this_play = Play.get_by_id(play_id) + + if batter_id is not None: + this_play.batter_id = batter_id + if pitcher_id is not None: + this_play.pitcher_id = pitcher_id + if catcher_id is not None: + this_play.catcher_id = catcher_id + if runner_id is not None: + this_play.runner_id = runner_id + if locked is not None: + this_play.locked = locked + if complete is not None: + this_play.complete = complete + if pa is not None: + this_play.pa = pa + if ab is not None: + this_play.ab = ab + if hit is not None: + this_play.hit = hit + if rbi is not None: + this_play.rbi = rbi + if double is not None: + this_play.double = double + if triple is not None: + this_play.triple = triple + if homerun is not None: + this_play.homerun = homerun + if starting_outs is not None: + this_play.starting_outs = starting_outs + if outs is not None: + this_play.outs = outs + if so is not None: + this_play.so = so + if bp1b is not None: + this_play.bp1b = bp1b + if bplo is not None: + this_play.bplo = bplo + if bphr is not None: + this_play.bphr = bphr + if bpfo is not None: + this_play.bpfo = bpfo + if walk is not None: + this_play.bb = walk + if hbp is not None: + this_play.hbp = hbp + if ibb is not None: + this_play.ibb = ibb + if sac is not None: + this_play.sac = sac + if sb is not None: + this_play.sb = sb + if cs is not None: + this_play.cs = cs + if wp is not None: + this_play.wild_pitch = wp + if pb is not None: + this_play.passed_ball = pb + if pick is not None: + this_play.pick_off = pick + if balk is not None: + this_play.balk = balk + if defender_id is not None: + this_play.defender_id = defender_id + if check_pos is not None: + this_play.check_pos = check_pos + if error is not None: + this_play.error = error + if play_num is not None: + this_play.play_num = play_num + if on_first_id is not None: + if not on_first_id: + this_play.on_first = None + else: + this_play.on_first_id = on_first_id + if on_first_final is not None: + if not on_first_final: + this_play.on_first_final = None + else: + this_play.on_first_final = on_first_final + if on_second_id is not None: + this_play.on_second_id = on_second_id + if not on_second_id: + this_play.on_second = None + else: + this_play.on_second_id = on_second_id + if on_second_final is not None: + this_play.on_second_final = on_second_final + if not on_second_final: + this_play.on_second_final = None + else: + this_play.on_second_final = on_second_final + if on_third_id is not None: + this_play.on_third_id = on_third_id + if not on_third_id: + this_play.on_third = None + else: + this_play.on_third_id = on_third_id + if on_third_final is not None: + this_play.on_third_final = on_third_final + if not on_third_final: + this_play.on_third_final = None + else: + this_play.on_third_final = on_third_final + + this_play.save() + # return_play = model_to_dict(this_play) + return_play = convert_stratplay(this_play) + db.close() + return return_play + +# TODO: UPDATE ALL PLAY REFERENCES TO USE THE STRATPLAY + + +def get_play_by_num(game_id, play_num: int): + latest_play = Play.get_or_none(Play.game_id == game_id, Play.play_num == play_num) + if not latest_play: + return None + + # return_play = model_to_dict(latest_play) + return_play = convert_stratplay(latest_play) + db.close() + return return_play + + +def get_latest_play(game_id): + latest_play = Play.select().where(Play.game_id == game_id).order_by(-Play.id).limit(1) + if not latest_play: + return None + + # return_play = model_to_dict(latest_play[0]) + return_play = convert_stratplay(latest_play[0]) + db.close() + return return_play + + +def undo_play(current_play_id): + this_play = Play.get_by_id(current_play_id) + last_play = Play.get_or_none(Play.game == this_play.game, Play.play_num == this_play.play_num - 1) + + count = 0 + count += this_play.delete_instance() + + last_play.complete = False + last_play.save() + + db.close() + + return count + + +def get_current_play(game_id) -> Optional[StratPlay]: + curr_play = Play.get_or_none(Play.game_id == game_id, Play.complete == False) + if not curr_play: + return None + + # return_play = model_to_dict(curr_play) + return_play = convert_stratplay(curr_play) + db.close() + return return_play + + +def get_last_inning_end_play(game_id, inning_half, inning_num): + this_play = Play.select().where( + (Play.game_id == game_id) & (Play.inning_half == inning_half) & (Play.inning_num == inning_num) + ).order_by(-Play.id) + + # return_play = model_to_dict(this_play[0]) + return_play = convert_stratplay(this_play[0]) + db.close() + return return_play + + +def advance_runners(play_id: int, num_bases: int, is_error: bool = False, only_forced: bool = False): + """ + Advances runners and tallies RBIs + """ + this_play = Play.get_by_id(play_id) + 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 + this_play.save() + db.close() + return + if this_play.on_second: + if this_play.on_third: + if num_bases > 0: + this_play.on_third_final = 4 + this_play.rbi += 1 if is_error else 0 + + if num_bases > 1: + this_play.on_second_final = 4 + this_play.rbi += 1 if 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 + this_play.rbi += 1 if 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 + this_play.rbi += 1 if 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 + this_play.rbi += 1 if 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 + this_play.rbi += 1 if 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.rbi += 1 + + this_play.save() + db.close() + + +def advance_one_runner(play_id: int, from_base: int, num_bases: int): + """ + Advances runner and excludes RBIs + """ + this_play = Play.get_by_id(play_id) + + if from_base == 1: + if num_bases > 2: + this_play.on_first_final = 4 + elif num_bases == 2: + this_play.on_first_final = 3 + elif num_bases == 1: + this_play.on_first_final = 2 + elif from_base == 2: + if num_bases > 1: + this_play.on_second_final = 4 + elif num_bases == 1: + this_play.on_second_final = 3 + elif from_base == 3: + if num_bases > 0: + this_play.on_third_final = 4 + + if this_play.on_first and not this_play.on_first_final: + this_play.on_first_final = 1 + if this_play.on_second and not this_play.on_second_final: + this_play.on_second_final = 2 + if this_play.on_third and not this_play.on_third_final: + this_play.on_third_final = 3 + + this_play.save() + db.close() + + +def complete_play(play_id, batter_to_base: int = None): + """ + Finalizes current play and sets base values for next play + """ + this_play = Play.get_by_id(play_id) + + this_play.locked = False + this_play.complete = True + this_play.save() + + logging.info(f'starting the inning calc') + new_inning_half = this_play.inning_half + new_inning_num = this_play.inning_num + if this_play.runner or this_play.wild_pitch or this_play.passed_ball or this_play.pick_off or this_play.balk: + new_batting_order = this_play.batting_order + else: + new_batting_order = this_play.batting_order + 1 if this_play.batting_order < 9 else 1 + new_bteam_id = this_play.batter.team_id + new_pteam_id = this_play.pitcher.team_id + new_starting_outs = this_play.starting_outs + this_play.outs + new_on_first = None + new_on_second = None + new_on_third = None + score_increment = this_play.homerun + + if this_play.starting_outs + this_play.outs > 2: + new_starting_outs = 0 + new_obc = 0 + if this_play.inning_half == 'Top': + new_inning_half = 'Bot' + new_bteam_id = this_play.game.home_team_id + new_pteam_id = this_play.game.away_team_id + else: + new_inning_half = 'Top' + new_inning_num += 1 + new_bteam_id = this_play.game.away_team_id + new_pteam_id = this_play.game.home_team_id + + if new_inning_num > 1: + last_inning_play = get_last_inning_end_play(this_play.game.id, new_inning_half, new_inning_num - 1) + new_batting_order = last_inning_play.batting_order + 1 if last_inning_play.batting_order < 9 else 1 + else: + new_batting_order = 1 + # Not an inning-ending play + else: + logging.info(f'starting the obc calc') + bases_occ = [False, False, False, False] + + # Set the occupied bases for the next play and lineup member occupying it + for runner, base in [ + (this_play.on_first, this_play.on_first_final), (this_play.on_second, this_play.on_second_final), + (this_play.on_third, this_play.on_third_final) + ]: + if base: + bases_occ[base - 1] = True + if base == 1: + new_on_first = runner + elif base == 2: + new_on_second = runner + elif base == 3: + new_on_third = runner + elif base == 4: + score_increment += 1 + + # Set the batter-runner occupied base for the next play + if batter_to_base: + if batter_to_base == 1: + bases_occ[0] = True + new_on_first = this_play.batter + elif batter_to_base == 2: + bases_occ[1] = True + new_on_second = this_play.batter + elif batter_to_base == 3: + bases_occ[2] = True + new_on_third = this_play.batter + elif batter_to_base == 4: + score_increment += 1 + this_play.batter_final = batter_to_base + this_play.save() + # Set the OBC based on the bases occupied + if bases_occ[2]: + if bases_occ[1]: + if bases_occ[0]: + new_obc = 7 + else: + new_obc = 6 + elif bases_occ[0]: + new_obc = 5 + else: + new_obc = 3 + elif bases_occ[1]: + if bases_occ[0]: + new_obc = 4 + else: + new_obc = 2 + elif bases_occ[0]: + new_obc = 1 + else: + new_obc = 0 + + if this_play.inning_half == 'Top': + new_away_score = this_play.away_score + score_increment + new_home_score = this_play.home_score + else: + new_away_score = this_play.away_score + new_home_score = this_play.home_score + score_increment + + batter = get_one_lineup(this_play.game.id, team_id=new_bteam_id, batting_order=new_batting_order) + batter_id = batter.id if batter else None + pitcher = get_one_lineup(this_play.game_id, team_id=new_pteam_id, position='P') + pitcher_id = pitcher.id if pitcher else None + + logging.info(f'done the obc calc') + next_play = Play.create(**{ + 'game_id': this_play.game.id, + 'play_num': this_play.play_num + 1, + 'batter_id': batter_id, + 'pitcher_id': pitcher_id, + 'on_base_code': new_obc, + 'inning_half': new_inning_half, + 'inning_num': new_inning_num, + 'next_inning_num': new_inning_num, + 'batting_order': new_batting_order, + 'starting_outs': new_starting_outs, + 'away_score': new_away_score, + 'home_score': new_home_score, + 'on_first': new_on_first, + 'on_second': new_on_second, + 'on_third': new_on_third + }) + # return_play = model_to_dict(next_play) + return_play = convert_stratplay(next_play) + db.close() + return return_play + + +def get_batting_stats(game_id, lineup_id: int = None, team_id: int = None): + batting_stats = Play.select( + Play.batter, + fn.SUM(Play.pa).over(partition_by=[Play.batter_id]).alias('pl_pa'), + fn.SUM(Play.ab).over(partition_by=[Play.batter_id]).alias('pl_ab'), + fn.SUM(Play.hit).over(partition_by=[Play.batter_id]).alias('pl_hit'), + fn.SUM(Play.rbi).over(partition_by=[Play.batter_id]).alias('pl_rbi'), + fn.SUM(Play.double).over(partition_by=[Play.batter_id]).alias('pl_double'), + fn.SUM(Play.triple).over(partition_by=[Play.batter_id]).alias('pl_triple'), + fn.SUM(Play.homerun).over(partition_by=[Play.batter_id]).alias('pl_homerun'), + fn.SUM(Play.bb).over(partition_by=[Play.batter_id]).alias('pl_bb'), + fn.SUM(Play.so).over(partition_by=[Play.batter_id]).alias('pl_so'), + fn.SUM(Play.hbp).over(partition_by=[Play.batter_id]).alias('pl_hbp'), + fn.SUM(Play.sac).over(partition_by=[Play.batter_id]).alias('pl_sac'), + fn.SUM(Play.ibb).over(partition_by=[Play.batter_id]).alias('pl_ibb'), + fn.SUM(Play.gidp).over(partition_by=[Play.batter_id]).alias('pl_gidp'), + fn.SUM(Play.sb).over(partition_by=[Play.batter_id]).alias('pl_sb'), + fn.SUM(Play.cs).over(partition_by=[Play.batter_id]).alias('pl_cs'), + fn.SUM(Play.bphr).over(partition_by=[Play.batter_id]).alias('pl_bphr'), + fn.SUM(Play.bpfo).over(partition_by=[Play.batter_id]).alias('pl_bpfo'), + fn.SUM(Play.bp1b).over(partition_by=[Play.batter_id]).alias('pl_bp1b'), + fn.SUM(Play.bplo).over(partition_by=[Play.batter_id]).alias('pl_bplo'), + fn.SUM(Play.pa).over(partition_by=[Play.batter.team_id]).alias('tm_pa'), + fn.SUM(Play.ab).over(partition_by=[Play.batter.team_id]).alias('tm_ab'), + fn.SUM(Play.hit).over(partition_by=[Play.batter.team_id]).alias('tm_hit'), + fn.SUM(Play.rbi).over(partition_by=[Play.batter.team_id]).alias('tm_rbi'), + fn.SUM(Play.double).over(partition_by=[Play.batter.team_id]).alias('tm_double'), + fn.SUM(Play.triple).over(partition_by=[Play.batter.team_id]).alias('tm_triple'), + fn.SUM(Play.homerun).over(partition_by=[Play.batter.team_id]).alias('tm_homerun'), + fn.SUM(Play.bb).over(partition_by=[Play.batter.team_id]).alias('tm_bb'), + fn.SUM(Play.so).over(partition_by=[Play.batter.team_id]).alias('tm_so'), + fn.SUM(Play.hbp).over(partition_by=[Play.batter.team_id]).alias('tm_hbp'), + fn.SUM(Play.sac).over(partition_by=[Play.batter.team_id]).alias('tm_sac'), + fn.SUM(Play.ibb).over(partition_by=[Play.batter.team_id]).alias('tm_ibb'), + fn.SUM(Play.gidp).over(partition_by=[Play.batter.team_id]).alias('tm_gidp'), + fn.SUM(Play.sb).over(partition_by=[Play.batter.team_id]).alias('tm_sb'), + fn.SUM(Play.cs).over(partition_by=[Play.batter.team_id]).alias('tm_ccs'), + fn.SUM(Play.bphr).over(partition_by=[Play.batter.team_id]).alias('tm_bphr'), + fn.SUM(Play.bpfo).over(partition_by=[Play.batter.team_id]).alias('tm_bpfo'), + fn.SUM(Play.bp1b).over(partition_by=[Play.batter.team_id]).alias('tm_bp1b'), + fn.SUM(Play.bplo).over(partition_by=[Play.batter.team_id]).alias('tm_bplo'), + ).join(Lineup, on=Play.batter).where(Play.game_id == game_id) + + if lineup_id is not None: + batting_stats = batting_stats.where(Play.batter_id == lineup_id) + elif team_id is not None: + batting_stats = batting_stats.where(Play.batter.team_id == team_id) + + done_batters = [] + return_batters = [] + for x in batting_stats: + if x.batter.id not in done_batters: + return_batters.append({ + 'batter_id': x.batter_id, + 'pl_pa': x.pl_pa, + 'pl_ab': x.pl_ab, + 'pl_hit': x.pl_hit, + 'pl_rbi': x.pl_rbi, + 'pl_double': x.pl_double, + 'pl_triple': x.pl_triple, + 'pl_homerun': x.pl_homerun, + 'pl_bb': x.pl_bb, + 'pl_so': x.pl_so, + 'pl_hbp': x.pl_hbp, + 'pl_sac': x.pl_sac, + 'pl_ibb': x.pl_ibb, + 'pl_gidp': x.pl_gidp, + 'pl_sb': x.pl_sb, + 'pl_cs': x.pl_cs, + 'pl_bphr': x.pl_bphr, + 'pl_bpfo': x.pl_bpfo, + 'pl_bp1b': x.pl_bp1b, + 'pl_bplo': x.pl_bplo, + }) + done_batters.append(x.batter.id) + + db.close() + logging.info(f'batting stats: {return_batters}') + return return_batters + + +def get_pitching_stats(game_id, lineup_id: int = None, team_id: int = None): + pitching_stats = Play.select( + Play.pitcher, + fn.SUM(Play.outs).over(partition_by=[Play.pitcher_id]).alias('pl_outs'), + fn.SUM(Play.hit).over(partition_by=[Play.pitcher_id]).alias('pl_hit'), + fn.COUNT(Play.on_first_final).filter( + Play.on_first_final == 4).over(partition_by=[Play.pitcher_id]).alias('pl_run_first'), + fn.COUNT(Play.on_second_final).filter( + Play.on_second_final == 4).over(partition_by=[Play.pitcher_id]).alias('pl_run_second'), + fn.COUNT(Play.on_third_final).filter( + Play.on_third_final == 4).over(partition_by=[Play.pitcher_id]).alias('pl_run_third'), + fn.COUNT(Play.batter_final).filter( + Play.batter_final == 4).over(partition_by=[Play.pitcher_id]).alias('pl_run_batter'), + fn.SUM(Play.so).over(partition_by=[Play.pitcher_id]).alias('pl_so'), + fn.SUM(Play.bb).over(partition_by=[Play.pitcher_id]).alias('pl_bb'), + fn.SUM(Play.hbp).over(partition_by=[Play.pitcher_id]).alias('pl_hbp'), + fn.SUM(Play.wild_pitch).over(partition_by=[Play.pitcher_id]).alias('pl_wild_pitch'), + fn.SUM(Play.balk).over(partition_by=[Play.pitcher_id]).alias('pl_balk'), + fn.SUM(Play.homerun).over(partition_by=[Play.pitcher_id]).alias('pl_homerun'), + fn.SUM(Play.outs).over(partition_by=[Play.pitcher.team_id]).alias('tm_outs'), + fn.SUM(Play.hit).over(partition_by=[Play.pitcher.team_id]).alias('tm_hit'), + fn.COUNT(Play.on_first_final).filter( + Play.on_first_final == 4).over(partition_by=[Play.pitcher.team_id]).alias('tm_run_first'), + fn.COUNT(Play.on_second_final).filter( + Play.on_second_final == 4).over(partition_by=[Play.pitcher.team_id]).alias('tm_run_second'), + fn.COUNT(Play.on_third_final).filter( + Play.on_third_final == 4).over(partition_by=[Play.pitcher.team_id]).alias('tm_run_third'), + fn.COUNT(Play.batter_final).filter( + Play.batter_final == 4).over(partition_by=[Play.pitcher.team_id]).alias('tm_run_batter'), + fn.SUM(Play.so).over(partition_by=[Play.pitcher.team_id]).alias('tm_so'), + fn.SUM(Play.bb).over(partition_by=[Play.pitcher.team_id]).alias('tm_bb'), + fn.SUM(Play.hbp).over(partition_by=[Play.pitcher.team_id]).alias('tm_hbp'), + fn.SUM(Play.wild_pitch).over(partition_by=[Play.pitcher.team_id]).alias('tm_wild_pitch'), + fn.SUM(Play.balk).over(partition_by=[Play.pitcher.team_id]).alias('tm_balk'), + fn.SUM(Play.homerun).over(partition_by=[Play.pitcher.team_id]).alias('tm_homerun'), + ).join(Lineup, on=Play.pitcher).where(Play.game_id == game_id) + + earned_runs_pl = Play.select().where( + ((Play.on_first_final == 4) | (Play.on_second_final == 4) | (Play.on_third_final == 4) | + (Play.batter_final == 4)) & (Play.error == 0) + ).join(Lineup, on=Play.pitcher).where(Play.game_id == game_id) + logging.info(f'earned_runs: {earned_runs_pl}') + earned_runs_tm = Play.select().where( + ((Play.on_first_final == 4) | (Play.on_second_final == 4) | (Play.on_third_final == 4) | + (Play.batter_final == 4)) & (Play.error == 0) + ).join(Lineup, on=Play.pitcher).where(Play.game_id == game_id) + + if lineup_id is not None: + pitching_stats = pitching_stats.where(Play.pitcher_id == lineup_id) + earned_runs_pl = earned_runs_pl.where(Play.pitcher_id == lineup_id) + earned_runs_tm = earned_runs_tm.where(Play.pitcher_id == lineup_id) + if team_id is not None: + pitching_stats = pitching_stats.where(Play.pitcher.team_id == team_id) + earned_runs_pl = earned_runs_pl.where(Play.pitcher.team_id == team_id) + earned_runs_tm = earned_runs_tm.where(Play.pitcher.team_id == team_id) + logging.info(f'query: {pitching_stats}') + logging.info(f'count: {pitching_stats.count()}') + logging.info(f'first entry: {pitching_stats[0]}') + + done_pitchers = [] + return_pitchers = [] + for x in pitching_stats: + logging.info(f'x: {x}') + if x.pitcher.id not in done_pitchers: + return_pitchers.append({ + 'pitcher_id': x.pitcher_id, + 'pl_outs': x.pl_outs, + 'pl_hit': x.pl_hit, + 'pl_eruns': earned_runs_pl.count(), + 'pl_runs': x.pl_run_first + x.pl_run_second + x.pl_run_third + x.pl_run_batter, + 'pl_so': x.pl_so, + 'pl_bb': x.pl_bb, + 'pl_hbp': x.pl_hbp, + 'pl_homerun': x.pl_homerun, + 'pl_wild_pitch': x.pl_wild_pitch, + 'pl_balk': x.pl_balk, + 'tm_outs': x.tm_outs, + 'tm_hit': x.tm_hit, + 'tm_eruns': earned_runs_tm.count(), + 'tm_runs': x.tm_run_first + x.tm_run_second + x.tm_run_third + x.tm_run_batter, + 'tm_so': x.tm_so, + 'tm_bb': x.tm_bb, + 'tm_hbp': x.tm_hbp, + 'tm_homerun': x.tm_homerun, + 'tm_wild_pitch': x.tm_wild_pitch, + 'tm_balk': x.tm_balk, + }) + done_pitchers.append(x.pitcher_id) + + db.close() + logging.info(f'pitching stats: {return_pitchers}') + return return_pitchers + diff --git a/gameplay_helpers.py b/gameplay_helpers.py new file mode 100644 index 0000000..4359471 --- /dev/null +++ b/gameplay_helpers.py @@ -0,0 +1,42 @@ +from db_calls_gameplay import StratGame, StratPlay, StratLineup, StratManagerAi, patch_play, advance_runners, \ + complete_play + + +def single_onestar(this_play: StratPlay, comp_play: bool = True): + patch_play(this_play.id, locked=True) + advance_runners(this_play.id, num_bases=1) + patch_play(this_play.id, pa=1, ab=1, hit=1) + if comp_play: + complete_play(this_play.id, batter_to_base=1) + + +def single_wellhit(this_play: StratPlay, comp_play: bool = True): + patch_play(this_play.id, locked=True) + advance_runners(this_play.id, num_bases=2) + patch_play(this_play.id, pa=1, ab=1, hit=1) + if comp_play: + complete_play(this_play.id, batter_to_base=1) + + +def double_twostar(this_play: StratPlay, comp_play: bool = True): + patch_play(this_play.id, locked=True) + advance_runners(this_play.id, num_bases=2) + patch_play(this_play.id, pa=1, ab=1, hit=1, double=1) + if comp_play: + complete_play(this_play.id, batter_to_base=2) + + +def double_threestar(this_play: StratPlay, comp_play: bool = True): + patch_play(this_play.id, locked=True) + advance_runners(this_play.id, num_bases=3) + patch_play(this_play.id, pa=1, ab=1, hit=1, double=1) + if comp_play: + complete_play(this_play.id, batter_to_base=2) + + +def triple(this_play: StratPlay, comp_play: bool = True): + patch_play(this_play.id, locked=True) + advance_runners(this_play.id, num_bases=3) + patch_play(this_play.id, pa=1, ab=1, hit=1, triple=1) + if comp_play: + complete_play(this_play.id, batter_to_base=3) diff --git a/helpers.py b/helpers.py new file mode 100644 index 0000000..f4c370e --- /dev/null +++ b/helpers.py @@ -0,0 +1,982 @@ +import datetime + +import pygsheets + +from db_calls import * + +import asyncio +import logging +import os +import random +import json + +import discord +import requests + +from discord.ext import commands +from difflib import get_close_matches + + +SBA_SEASON = 7 +PD_SEASON = 4 +SBA_COLOR = 'a6ce39' + +SBA_ROSTER_KEY = '1bt7LLJe6h7axkhDVlxJ4f319l8QmFB0zQH-pjM0c8a8' +SBA_STATS_KEY = '1fnqx2uxC7DT5aTnx4EkXh83crwrL0W6eJefoC1d4KH4' +SBA_STANDINGS_KEY = '1cXZcPY08RvqV_GeLvZ7PY5-0CyM-AijpJxsaFisZjBc' +SBA_ROSTER_URL = 'https://docs.google.com/spreadsheets/d/10fKx1vQ7tEjKx0OD5tnFADdjsUeFbqxMIshz2d5i-Is/edit' +SBA_BASE_URL = 'https://sombaseball.ddns.net' +SBA_SEASON4_DRAFT_KEY = '1lztxahGRypykfWGKd2duDGFH0omclLqFLSt-vwqdSu8' +SBA_SEASON5_DRAFT_KEY = '1euuKeWqQEUmE9OiF9wihO5LMERWP3Zwg_KsG2w-Kx54' +SBA_SEASON6_DRAFT_KEY = '13_xWG1wQy7G4UJvohD8JIUBE-7yuWT9lVta1rkAlHQE' +SBA_SEASON7_DRAFT_KEY = '1BgySsUlQf9K21_uOjQOY7O0GrRfF6zt1BBaEFlvBokY' +SBA_STANDINGS_URL = f'{SBA_BASE_URL}/standings' +SBA_SCHEDULE_URL = f'{SBA_BASE_URL}/schedule' +SBA_IMAGE_URL = f'{SBA_BASE_URL}/static/images' +SBA_PLAYERS_ROLE_NAME = f'Season {SBA_SEASON} Players' +PD_PLAYERS_ROLE_NAME = f'Paper Dynasty Players' +ALL_PLAYERS = [SBA_PLAYERS_ROLE_NAME, PD_PLAYERS_ROLE_NAME] + +LOGO = 'https://sombaseball.ddns.net/static/images/sba-logo.png' +INFIELD_X_CHART = { + 'si1': { + 'rp': 'Runner on first: Line drive hits the runner! Runner on first is out. Batter goes to first with single ' + 'and all other runners hold.\nNo runner on first: batter singles, runners advance 1 base.', + 'e1': 'Single and Error, batter to second, runners advance 2 bases.', + 'e2': 'Single and Error, batter to third, all runners score.', + 'no': 'Single, runners advance 1 base.' + }, + 'po': { + 'rp': 'The batters hits a popup. None of the fielders take charge on the play and the ball drops in the ' + 'infield for a single! All runners advance 1 base.', + 'e1': 'The catcher drops a popup for an error. All runners advance 1 base.', + 'e2': 'The catcher grabs a squib in front of the plate and throws it into right field. The batter goes to ' + 'second and all runners score.', + 'no': 'The batter pops out to the catcher.' + }, + 'wp': { + 'rp': 'Automatic wild pitch. Catcher has trouble finding it and all base runners advance 2 bases.', + 'no': 'Automatic wild pitch, all runners advance 1 base and batter rolls AB again.' + }, + 'x': { + 'rp': 'Runner(s) on base: pitcher trips during his delivery and the ball sails for automatic wild pitch, ' + 'runners advance 1 base and batter rolls AB again.', + 'no': 'Wild pitch check (credited as a PB). If a passed ball occurs, batter rerolls AB. ' + 'If no passed ball occurs, the batter fouls out to the catcher.' + }, + 'fo': { + 'rp': 'Batter swings and misses, but is awarded first base on a catcher interference call! Baserunners advance ' + 'only if forced.', + 'e1': 'The catcher drops a foul popup for an error. Batter rolls AB again.', + 'e2': 'The catcher drops a foul popup for an error. Batter rolls AB again.', + 'no': 'Runner(s) on base: make a passed ball check. If no passed ball, batter pops out to the catcher. If a ' + 'passed ball occurs, batter roll his AB again.\nNo runners: batter pops out to the catcher' + }, + 'g1': { + 'rp': 'Runner on first: runner on first breaks up the double play, but umpires call runner interference and ' + 'the batter is out on GIDP.\nNo runners: Batter grounds out.', + 'e1': 'Error, batter to first, runners advance 1 base.', + 'e2': 'Error, batter to second, runners advance 2 bases.', + 'no': 'Consult Groundball Chart: `!gbA`' + }, + 'g2': { + 'rp': 'Batter lines the ball off the pitcher to the fielder who makes the play to first for the out! Runners ' + 'advance only if forced.', + 'e1': 'Error, batter to first, runners advance 1 base.', + 'e2': 'Error, batter to second, runners advance 2 bases.', + 'no': 'Consult Groundball Chart: `!gbB`' + }, + 'g3': { + 'rp': 'Batter lines the ball off the mound and deflects to the fielder who makes the play to first for the ' + 'out! Runners advance 1 base.', + 'e1': 'Error, batter to first, runners advance 1 base.', + 'e2': 'Error, batter to second, runners advance 2 bases.', + 'no': 'Consult Groundball Chart: `!gbC`' + }, +} +OUTFIELD_X_CHART = { + 'si2': { + 'rp': 'Batter singles, baserunners advance 2 bases. As the batter rounds first, the fielder throws behind him ' + 'and catches him off the bag for an out!', + 'e1': 'Single and error, batter to second, runners advance 2 bases.', + 'e2': 'Single and error, batter to third, all runners score.', + 'e3': 'Single and error, batter to third, all runners score', + 'no': 'Single, all runners advance 2 bases.' + }, + 'do2': { + 'rp': 'Batter doubles, runners advance 2 bases. The outfielder throws the ball to the shortstop who executes a ' + 'hidden ball trick! Runner on second is called out!', + 'e1': 'Double and error, batter to third, all runners score.', + 'e2': 'Double and error, batter to third, and all runners score.', + 'e3': 'Double and error, batter and all runners score. Little league home run!', + 'no': 'Double, all runners advance 2 bases.' + }, + 'do3': { + 'rp': 'Runner(s) on base: batter doubles and runners advance three bases as the outfielders collide!\n' + 'No runners: Batter doubles, but the play is appealed. The umps rule the batter missed first base so is ' + 'out on the appeal!', + 'e1': 'Double and error, batter to third, all runners score.', + 'e2': 'Double and error, batter and all runners score. Little league home run!', + 'e3': 'Double and error, batter and all runners score. Little league home run!', + 'no': 'Double, all runners score.' + }, + 'tr3': { + 'rp': 'Batter hits a ball into the gap and the outfielders collide trying to make the play! The ball rolls to ' + 'the wall and the batter trots home with an inside-the-park home run!', + 'e1': 'Triple and error, batter and all runners score. Little league home run!', + 'e2': 'Triple and error, batter and all runners score. Little league home run!', + 'e3': 'Triple and error, batter and all runners score. Little league home run!', + 'no': 'Triple, all runners score.' + }, + 'f1': { + 'rp': 'The outfielder races back and makes a diving catch and collides with the wall! In the time he takes to ' + 'recuperate, all baserunners tag-up and advance 2 bases.', + 'e1': '1 base error, runners advance 1 base.', + 'e2': '2 base error, runners advance 2 bases.', + 'e3': '3 base error, batter to third, all runners score.', + 'no': 'Flyball A' + }, + 'f2': { + 'rp': 'The outfielder catches the flyball for an out. If there is a runner on third, he tags-up and scores. ' + 'The play is appealed and the umps rule that the runner left early and is out on the appeal!', + 'e1': '1 base error, runners advance 1 base.', + 'e2': '2 base error, runners advance 2 bases.', + 'e3': '3 base error, batter to third, all runners score.', + 'no': 'Flyball B' + }, + 'f3': { + 'rp': 'The outfielder makes a running catch in the gap! The lead runner lost track of the ball and was ' + 'advancing - he cannot return in time and is doubled off by the outfielder.', + 'e1': '1 base error, runners advance 1 base.', + 'e2': '2 base error, runners advance 2 bases.', + 'e3': '3 base error, batter to third, all runners score.', + 'no': 'Flyball C' + } +} + + +class Question: + def __init__(self, bot, channel, prompt, qtype, timeout, embed=None): + """ + Version 0.4 + + :param bot, discord bot object + :param channel, discord Channel object + :param prompt, string, prompt message to post + :param qtype, string, 'yesno', 'int', 'float', 'text', or 'url' + :param timeout, float, time to wait for a response + """ + if not prompt and not embed: + raise TypeError('prompt and embed may not both be None') + + self.bot = bot + self.channel = channel + self.prompt = prompt + self.qtype = qtype + self.timeout = timeout + self.embed = embed + + async def ask(self, responders: list): + """ + Params: responder, list of discord User objects + Returns: True/False if confirm question; full response otherwise + """ + yes = [ + 'yes', 'y', 'ye', 'yee', 'yerp', 'yep', 'yeet', 'yip', 'yup', 'yarp', 'si', 'fine', 'sure', 'k', 'ok', + 'okay' + ] + no = ['no', 'n', 'nope', 'nah', 'nyet', 'nein'] + + if type(responders) is not list: + raise TypeError('Responders must be a list of Members') + + def yesno(mes): + return mes.channel == self.channel and mes.author in responders and mes.content.lower() in yes + no + + def text(mes): + return mes.channel == self.channel and mes.author in responders + + await self.channel.send(content=self.prompt, embed=self.embed) + + try: + resp = await self.bot.wait_for( + 'message', + timeout=self.timeout, + check=yesno if self.qtype == 'yesno' else text + ) + except asyncio.TimeoutError: + return None + except Exception as e: + await self.channel.send(f'Yuck, do you know what this means?\n\n{e}') + return None + + if self.qtype == 'yesno': + if resp.content.lower() in yes: + return True + else: + return False + elif self.qtype == 'int': + return int(resp.content) + elif self.qtype == 'float': + return float(resp.content) + elif self.qtype == 'url': + if resp.content[:7] == 'http://' or resp.content[:8] == 'https://': + return resp.content + else: + return False + else: + return resp.content + + +# Define a simple View that gives us a confirmation menu +class Confirm(discord.ui.View): + def __init__(self, responders: list, timeout: float = 300.0): + super().__init__(timeout=timeout) + if not isinstance(responders, list): + raise TypeError('responders must be a list') + self.value = None + self.responders = responders + + # When the confirm button is pressed, set the inner value to `True` and + # stop the View from listening to more input. + # We also send the user an ephemeral message that we're confirming their choice. + @discord.ui.button(label='Confirm', style=discord.ButtonStyle.green) + async def confirm(self, interaction: discord.Interaction, button: discord.ui.Button): + if interaction.user not in self.responders: + return + + # await interaction.response.send_message('Confirmed', ephemeral=True) + self.value = True + self.clear_items() + self.stop() + + # This one is similar to the confirmation button except sets the inner value to `False` + @discord.ui.button(label='Cancel', style=discord.ButtonStyle.grey) + async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button): + if interaction.user not in self.responders: + return + + # await interaction.response.send_message('Cancelled', ephemeral=True) + self.value = False + self.clear_items() + self.stop() + + +class Pagination(discord.ui.View): + def __init__(self, responders: list): + super().__init__() + if not isinstance(responders, list): + raise TypeError('responders must be a list') + + self.value = None + self.responders = responders + + @discord.ui.button(label='⏮️', style=discord.ButtonStyle.grey) + async def left_button(self, interaction: discord.Interaction, button: discord.ui.Button): + if interaction.user not in self.responders: + logging.info(f'{interaction.user} is not in {self.responders}') + return + + self.value = 'left' + await interaction.response.defer() + self.stop() + + @discord.ui.button(label='⏭️', style=discord.ButtonStyle.grey) + async def right_button(self, interaction: discord.Interaction, button: discord.ui.Button): + if interaction.user not in self.responders: + logging.info(f'{interaction.user} is not in {self.responders}') + return + + self.value = 'right' + await interaction.response.defer() + self.stop() + + +def get_xchart_data(code: str): + result = '' + if code in INFIELD_X_CHART.keys(): + for key in INFIELD_X_CHART[code]: + result += f'**{key.upper()}**:\n{INFIELD_X_CHART[code][key]}\n\n' + return result + elif code in OUTFIELD_X_CHART.keys(): + for key in OUTFIELD_X_CHART[code]: + result += f'**{key.upper()}**:\n{OUTFIELD_X_CHART[code][key]}\n\n' + return result + + return None + + +def get_groundball_embeds(this_command): + url_base = 'https://sombaseball.ddns.net/static/images/season04/ground-ball-chart' + + if this_command in ['g1', 'g2', 'g3']: + embed = discord.Embed(title=f'Groundball {this_command.upper()} Chart') + embed.set_image(url=f'{url_base}-{this_command}.png') + + return [embed] + else: + embed1 = discord.Embed(title='Groundball Chart 1/2') + embed1.set_image(url=f'{url_base}01.png') + embed2 = discord.Embed(title='Groundball Chart 2/2') + embed2.set_image(url=f'{url_base}02.png') + + return [embed1, embed2] + + +def old_rand_conf_gif(): + conf_gifs = [ + 'https://tenor.com/view/boom-annakendrick-pitchperfect-pitchperfect2-micdrop-gif-5143507', + 'https://tenor.com/view/boom-annakendrick-pitchperfect-pitchperfect2-micdrop-gif-5143507', + 'https://tenor.com/view/boom-annakendrick-pitchperfect-pitchperfect2-micdrop-gif-5143507', + 'https://tenor.com/view/explosion-boom-iron-man-gif-14282225', + 'https://tenor.com/view/betty-white-dab-consider-it-done-gif-11972415', + 'https://tenor.com/view/done-and-done-spongebob-finished-just-did-it-gif-10843280', + 'https://tenor.com/view/thumbs-up-okay-ok-well-done-gif-13840394', + 'https://tenor.com/view/tinkerbell-peter-pan-all-done-gif-15003723', + 'https://tenor.com/view/done-and-done-ron-swanson-gotchu-gif-10843254', + 'https://tenor.com/view/sponge-bob-thumbs-up-ok-smile-gif-12038157', + 'https://tenor.com/view/thumbs-up-cool-okay-bye-gif-8633196', + 'https://media.giphy.com/media/zCME2Cd20Czvy/giphy.gif', + 'https://media.giphy.com/media/xT0xeJpckCr0qGAFna/giphy.gif', + 'https://media0.giphy.com/media/l0MYw3oeYCUJhj5FC/200.gif', + 'https://media2.giphy.com/media/52FcaTVc9Y1rk7q1NQ/200.gif', + 'https://i0.wp.com/media1.giphy.com/media/iwvuPyfi7z14I/giphy.gif', + 'https://media1.tenor.com/images/859a2d3b201fbacec13904242976b9e0/tenor.gif', + 'https://media.giphy.com/media/3o6ZsZbUukGiYMf2a4/giphy.gif', + 'https://media.giphy.com/media/l0Iyl55kTeh71nTXy/giphy.gif', + 'https://media.giphy.com/media/3o7qDEq2bMbcbPRQ2c/giphy.gif', + 'https://media.giphy.com/media/3orieTbyzN9QNCDANa/giphy.gif', + 'https://media.giphy.com/media/l3V0JGUuz4xght7Py/giphy.gif', + 'https://media.giphy.com/media/o1H5mqZKB0RCE/giphy.gif', + 'https://media.giphy.com/media/Rhf0uSWt1P2TFqVMZK/giphy.gif', + 'https://media.giphy.com/media/UIMAFTRUcf6V71BGsd/giphy.gif', + 'https://media.giphy.com/media/xUOwFYnEb6rv4Oxx6M/giphy.gif', + 'https://media.giphy.com/media/6bdiLSKOwgR6ekaTSS/giphy.gif', + 'https://tenor.com/bc1OJ.gif', + 'https://tenor.com/1EmF.gif', + 'https://tenor.com/ZYCh.gif', + 'https://tenor.com/patd.gif', + 'https://tenor.com/u6mU.gif', + 'https://tenor.com/x2sa.gif', + 'https://tenor.com/bAVeS.gif', + 'https://tenor.com/bxOcj.gif', + 'https://tenor.com/ETJ7.gif', + 'https://tenor.com/bpH3g.gif', + 'https://tenor.com/biF9q.gif', + 'https://tenor.com/OySS.gif', + 'https://tenor.com/bvVFv.gif', + 'https://tenor.com/bFeqA.gif' + ] + return conf_gifs[random.randint(0, len(conf_gifs) - 1)] + + +def random_conf_gif(): + req_url = 'https://api.giphy.com/v1/gifs/translate?s=all done&api_key=H86xibttEuUcslgmMM6uu74IgLEZ7UOD' + + resp = requests.get(req_url, timeout=3) + if resp.status_code == 200: + data = resp.json() + if 'trump' in data['data']['title']: + return old_rand_conf_gif() + else: + return data['data']['url'] + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') + + +def random_salute_gif(): + salute_gifs = [ + 'https://media.giphy.com/media/fSAyceY3BCgtiQGnJs/giphy.gif', + 'https://media.giphy.com/media/bsWDUSFUmJCOk/giphy.gif', + 'https://media.giphy.com/media/hStvd5LiWCFzYNyxR4/giphy.gif', + 'https://media.giphy.com/media/RhSR5xXDsXJ7jbnrRW/giphy.gif', + 'https://media.giphy.com/media/lNQvrlPdbmZUU2wlh9/giphy.gif', + 'https://gfycat.com/skeletaldependableandeancat', + 'https://i.gifer.com/5EJk.gif', + 'https://tenor.com/baJUV.gif', + 'https://tenor.com/bdnQH.gif', + 'https://tenor.com/bikQU.gif', + 'https://i.pinimg.com/originals/04/36/bf/0436bfc9861b4b57ffffda82d3adad6e.gif', + 'https://media.giphy.com/media/6RtOG4Q7v34kw/giphy.gif', + 'https://keyassets-p2.timeincuk.net/wp/prod/wp-content/uploads/sites/42/2017/04/anigif_' + 'enhanced-946-1433453114-7.gif', + 'https://keyassets-p2.timeincuk.net/wp/prod/wp-content/uploads/sites/42/2017/04/100c5d677cc28ea3f15' + '4c70d641f655b_meme-crying-gif-crying-gif-meme_620-340.gif', + 'https://media.giphy.com/media/fnKd6rCHaZoGdzLjjA/giphy.gif', + 'https://media.giphy.com/media/47D5jmVc4f7ylygXYD/giphy.gif', + 'https://media.giphy.com/media/I4wGMXoi2kMDe/giphy.gif', + ] + return salute_gifs[random.randint(0, len(salute_gifs) - 1)] + + +def random_conf_word(): + conf_words = [ + 'dope', + 'cool', + 'got it', + 'noice', + 'ok', + 'lit', + ] + return conf_words[random.randint(0, len(conf_words) - 1)] + + +def random_codename(): + all_names = [ + 'Shong', 'DerekSux', 'JoeSux', 'CalSux', 'Friend', 'Andrea', 'Ent', 'Lindved', 'Camp', 'Idyll', 'Elaphus', + 'Turki', 'Shrimp', 'Primary', 'Anglica', 'Shail', 'Blanket', 'Baffled', 'Deer', 'Thisted', 'Brisk', 'Shy', + 'Table', 'Jorts', 'Renati', 'Gisky', 'Prig', 'Bathtub', 'Gallery', 'Mavas', 'Chird', 'Oxyura', 'Mydal', 'Brown', + 'Vasen', 'Worthy', 'Bivver', 'Cirlus', 'Self', 'Len', 'Sharp', 'Dart', 'Crepis', 'Ferina', 'Curl', 'Lancome', + 'Stuff', 'Glove', 'Consist', 'Smig', 'Egg', 'Pleat', 'Picture', 'Spin', 'Ridgty', 'Ickled', 'Abashed', 'Haul', + 'Cordage', 'Chivery', 'Stointy', 'Baa', 'Here', 'Ulmi', 'Tour', 'Tribe', 'Crunch', 'Used', 'Pigface', 'Audit', + 'Written', 'Once', 'Fickle', 'Drugged', 'Swarm', 'Blimber', 'Torso', 'Retusa', 'Hockey', 'Pusty', 'Sallow', + 'Next', 'Mansion', 'Glass', 'Screen', 'Josiah', 'Bonkey', 'Stuff', 'Sane', 'Blooded', 'Gnat', 'Liparis', + 'Ocean', 'Sway', 'Roband', 'Still', 'Ribba', 'Biryani', 'Halibut', 'Flyn', 'Until', 'Depend', 'Intel', + 'Affinis', 'Chef', 'Trounce', 'Crawl', 'Grab', 'Eggs', 'Malfroy', 'Sitta', 'Cretin', 'May', 'Smithii', + 'Saffron', 'Crummy', 'Powered', 'Rail', 'Trait', 'Koiled', 'Bronze', 'Quickly', 'Vikis', 'Trift', 'Jubilar', + 'Deft', 'Juncus', 'Sodding', 'Distant', 'Poecile', 'Pipe', 'Sell', 'Inops', 'Peusi', 'Sparrow', 'Yams', + 'Kidneys', 'Artery', 'Vuffin', 'Boink', 'Bos', 'Notable', 'Alba', 'Spurge', 'Ruby', 'Cilia', 'Pellow', 'Nox', + 'Woozy', 'Semvik', 'Tyda', 'Season', 'Lychnis', 'Ibestad', 'Bagge', 'Marked', 'Browdie', 'Fisher', 'Tilly', + 'Troll', 'Gypsy', 'Thisted', 'Flirt', 'Stop', 'Radiate', 'Poop', 'Plenty', 'Jeff', 'Magpie', 'Roof', 'Ent', + 'Dumbo', 'Pride', 'Weights', 'Winted', 'Dolden', 'Meotica', 'Yikes', 'Teeny', 'Fizz', 'Eide', 'Foetida', + 'Crash', 'Mann', 'Salong', 'Cetti', 'Balloon', 'Petite', 'Find', 'Sputter', 'Patula', 'Upstage', 'Aurora', + 'Dadson', 'Drate', 'Heidal', 'Robin', 'Auditor', 'Ithil', 'Warmen', 'Pat', 'Muppet', '007', 'Advantage', + 'Alert', 'Backhander', 'Badass', 'Blade', 'Blaze', 'Blockade', 'Blockbuster', 'Boxer', 'Brimstone', 'Broadway', + 'Buccaneer', 'Champion', 'Cliffhanger', 'Coachman', 'Comet', 'Commander', 'Courier', 'Cowboy', 'Crawler', + 'Crossroads', 'DeepSpace', 'Desperado', 'Double-Decker', 'Echelon', 'Edge', 'Encore', 'EnRoute', 'Escape', + 'Eureka', 'Evangelist', 'Excursion', 'Explorer', 'Fantastic', 'Firefight', 'Foray', 'Forge', 'Freeway', + 'Frontier', 'FunMachine', 'Galaxy', 'GameOver', 'Genesis', 'Hacker', 'Hawkeye', 'Haybailer', 'Haystack', + 'Hexagon', 'Hitman', 'Hustler', 'Iceberg', 'Impossible', 'Impulse', 'Invader', 'Inventor', 'IronWolf', + 'Jackrabbit', 'Juniper', 'Keyhole', 'Lancelot', 'Liftoff', 'MadHatter', 'Magnum', 'Majestic', 'Merlin', + 'Multiplier', 'Netiquette', 'Nomad', 'Octagon', 'Offense', 'OliveBranch', 'OlympicTorch', 'Omega', 'Onyx', + 'Orbit', 'OuterSpace', 'Outlaw', 'Patron', 'Patriot', 'Pegasus', 'Pentagon', 'Pilgrim', 'Pinball', 'Pinnacle', + 'Pipeline', 'Pirate', 'Portal', 'Predator', 'Prism', 'RagingBull', 'Ragtime', 'Reunion', 'Ricochet', + 'Roadrunner', 'Rockstar', 'RobinHood', 'Rover', 'Runabout', 'Sapphire', 'Scrappy', 'Seige', 'Shadow', + 'Shakedown', 'Shockwave', 'Shooter', 'Showdown', 'SixPack', 'SlamDunk', 'Slasher', 'Sledgehammer', 'Spirit', + 'Spotlight', 'Starlight', 'Steamroller', 'Stride', 'Sunrise', 'Superhuman', 'Supernova', 'SuperBowl', 'Sunset', + 'Sweetheart', 'TopHand', 'Touchdown', 'Tour', 'Trailblazer', 'Transit', 'Trekker', 'Trio', 'TriplePlay', + 'TripleThreat', 'Universe', 'Unstoppable', 'Utopia', 'Vicinity', 'Vector', 'Vigilance', 'Vigilante', 'Vista', + 'Visage', 'Vis-à-vis', 'VIP', 'Volcano', 'Volley', 'Whizzler', 'Wingman', 'Badger', 'BlackCat', 'Bobcat', + 'Caracal', 'Cheetah', 'Cougar', 'Jaguar', 'Leopard', 'Lion', 'Lynx', 'MountainLion', 'Ocelot', 'Panther', + 'Puma', 'Siamese', 'Serval', 'Tiger', 'Wolverine', 'Abispa', 'Andrena', 'BlackWidow', 'Cataglyphis', + 'Centipede', 'Cephalotes', 'Formica', 'Hornet', 'Jellyfish', 'Scorpion', 'Tarantula', 'Yellowjacket', 'Wasp', + 'Apollo', 'Ares', 'Artemis', 'Athena', 'Hercules', 'Hermes', 'Iris', 'Medusa', 'Nemesis', 'Neptune', 'Perseus', + 'Poseidon', 'Triton', 'Zeus', 'Aquarius', 'Aries', 'Cancer', 'Capricorn', 'Gemini', 'Libra', 'Leo', 'Pisces', + 'Sagittarius', 'Scorpio', 'Taurus', 'Virgo', 'Andromeda', 'Aquila', 'Cassiopeia', 'Cepheus', 'Cygnus', + 'Delphinus', 'Drako', 'Lyra', 'Orion', 'Perseus', 'Serpens', 'Triangulum', 'Anaconda', 'Boa', 'Cobra', + 'Copperhead', 'Cottonmouth', 'Garter', 'Kingsnake', 'Mamba', 'Python', 'Rattler', 'Sidewinder', 'Taipan', + 'Viper', 'Alligator', 'Barracuda', 'Crocodile', 'Gator', 'GreatWhite', 'Hammerhead', 'Jaws', 'Lionfish', + 'Mako', 'Moray', 'Orca', 'Piranha', 'Shark', 'Stingray', 'Axe', 'BattleAxe', 'Bayonet', 'Blade', 'Crossbowe', + 'Dagger', 'Excalibur', 'Halberd', 'Hatchet', 'Machete', 'Saber', 'Samurai', 'Scimitar', 'Scythe', 'Stiletto', + 'Spear', 'Sword', 'Aurora', 'Avalanche', 'Blizzard', 'Cyclone', 'Dewdrop', 'Downpour', 'Duststorm', 'Fogbank', + 'Freeze', 'Frost', 'GullyWasher', 'Gust', 'Hurricane', 'IceStorm', 'JetStream', 'Lightning', 'Mist', 'Monsoon', + 'Rainbow', 'Raindrop', 'SandStorm', 'Seabreeze', 'Snowflake', 'Stratosphere', 'Storm', 'Sunrise', 'Sunset', + 'Tornado', 'Thunder', 'Thunderbolt', 'Thunderstorm', 'TropicalStorm', 'Twister', 'Typhoon', 'Updraft', 'Vortex', + 'Waterspout', 'Whirlwind', 'WindChill', 'Archimedes', 'Aristotle', 'Confucius', 'Copernicus', 'Curie', + 'daVinci', 'Darwin', 'Descartes', 'Edison', 'Einstein', 'Epicurus', 'Freud', 'Galileo', 'Hawking', + 'Machiavelli', 'Marx', 'Newton', 'Pascal', 'Pasteur', 'Plato', 'Sagan', 'Socrates', 'Tesla', 'Voltaire', + 'Baccarat', 'Backgammon', 'Blackjack', 'Chess', 'Jenga', 'Jeopardy', 'Keno', 'Monopoly', 'Pictionary', 'Poker', + 'Scrabble', 'TrivialPursuit', 'Twister', 'Roulette', 'Stratego', 'Yahtzee', 'Aquaman', 'Batman', 'BlackPanther', + 'BlackWidow', 'CaptainAmerica', 'Catwoman', 'Daredevil', 'Dr.Strange', 'Flash', 'GreenArrow', 'GreenLantern', + 'Hulk', 'IronMan', 'Phantom', 'Thor', 'SilverSurfer', 'SpiderMan', 'Supergirl', 'Superman', 'WonderWoman', + 'Wolverine', 'Hypersonic', 'Lightspeed', 'Mach1,2,3,4,etc', 'Supersonic', 'WarpSpeed', 'Amiatina', 'Andalusian', + 'Appaloosa', 'Clydesdale', 'Colt', 'Falabella', 'Knabstrupper', 'Lipizzan', 'Lucitano', 'Maverick', 'Mustang', + 'Palomino', 'Pony', 'QuarterHorse', 'Stallion', 'Thoroughbred', 'Zebra', 'Antigua', 'Aruba', 'Azores', 'Baja', + 'Bali', 'Barbados', 'Bermuda', 'BoraBora', 'Borneo', 'Capri', 'Cayman', 'Corfu', 'Cozumel', 'Curacao', 'Fiji', + 'Galapagos', 'Hawaii', 'Ibiza', 'Jamaica', 'Kauai', 'Lanai', 'Majorca', 'Maldives', 'Maui', 'Mykonos', + 'Nantucket', 'Oahu', 'Tahiti', 'Tortuga', 'Roatan', 'Santorini', 'Seychelles', 'St.Johns', 'St.Lucia', + 'Albatross', 'BaldEagle', 'Blackhawk', 'BlueJay', 'Chukar', 'Condor', 'Crane', 'Dove', 'Eagle', 'Falcon', + 'Goose(GoldenGoose)', 'Grouse', 'Hawk', 'Heron', 'Hornbill', 'Hummingbird', 'Lark', 'Mallard', 'Oriole', + 'Osprey', 'Owl', 'Parrot', 'Penguin', 'Peregrine', 'Pelican', 'Pheasant', 'Quail', 'Raptor', 'Raven', 'Robin', + 'Sandpiper', 'Seagull', 'Sparrow', 'Stork', 'Thunderbird', 'Toucan', 'Vulture', 'Waterfowl', 'Woodpecker', + 'Wren', 'C-3PO', 'Chewbacca', 'Dagobah', 'DarthVader', 'DeathStar', 'Devaron', 'Droid', 'Endor', 'Ewok', 'Hoth', + 'Jakku', 'Jedi', 'Leia', 'Lightsaber', 'Lothal', 'Naboo', 'Padawan', 'R2-D2', 'Scarif', 'Sith', 'Skywalker', + 'Stormtrooper', 'Tatooine', 'Wookie', 'Yoda', 'Zanbar', 'Canoe', 'Catamaran', 'Cruiser', 'Cutter', 'Ferry', + 'Galleon', 'Gondola', 'Hovercraft', 'Hydrofoil', 'Jetski', 'Kayak', 'Longboat', 'Motorboat', 'Outrigger', + 'PirateShip', 'Riverboat', 'Sailboat', 'Skipjack', 'Schooner', 'Skiff', 'Sloop', 'Steamboat', 'Tanker', + 'Trimaran', 'Trawler', 'Tugboat', 'U-boat', 'Yacht', 'Yawl', 'Lancer', 'Volunteer', 'Searchlight', 'Passkey', + 'Deacon', 'Rawhide', 'Timberwolf', 'Eagle', 'Tumbler', 'Renegade', 'Mogul' + ] + + this_name = all_names[random.randint(0, len(all_names) - 1)] + return this_name + + +def random_no_phrase(): + phrases = [ + 'uhh...no', + 'lol no', + 'nope', + ] + + +def random_soccer(): + all_phrases = [ + 'Ugh, this again', + 'Bro I can\'t with this', + 'Fucking soccer again?', + 'Gtfo with this soccer stuff', + 'I\'m just gonna start deleting stuff', + 'No\nNo\n\nNooooo\n\n\nFucking no more soccer\n\n\n\n\n\nNooooooooooooooooooo', + 'Omg with this soccer shit again', + 'Do you ever want to win an sba game again?' + ] + this_phrase = all_phrases[random.randint(0, len(all_phrases) - 1)] + return this_phrase + + +async def get_emoji(ctx, name, return_empty=True): + try: + emoji = await commands.converter.EmojiConverter().convert(ctx, name) + except: + if return_empty: + emoji = '' + else: + return name + return emoji + + +async def team_emoji(ctx, team): + try: + emoji = await get_emoji(ctx, f'{team["sname"].lower().replace(" ","")}') + except: + emoji = '' + return emoji + + +async def fuzzy_player_search(ctx, channel, bot, name, master_list): + """ + Takes a name to search and returns the name of the best match + + :param ctx: discord context + :param channel: discord channel + :param bot: discord.py bot object + :param name: string + :param master_list: list of names + :return: + """ + logging.warning(f'fuzzy player search - len(master_list): {len(master_list)}') + if name.lower() in master_list: + return name.lower() + + great_matches = get_close_matches(name, master_list, cutoff=0.8) + if len(great_matches) == 1: + return great_matches[0] + elif len(great_matches) > 0: + matches = great_matches + else: + matches = get_close_matches(name, master_list, n=6) + if len(matches) == 1: + return matches[0] + + if not matches: + raise ValueError(f'{name.title()} was not found') + + embed = discord.Embed( + title="Did You Mean...", + description='Enter the number of the card you would like to see.', + color=0x7FC600 + ) + count = 1 + for x in matches: + embed.add_field(name=f'{count}', value=x, inline=False) + count += 1 + embed.set_footer(text='These are the closest matches. Spell better if they\'re not who you want.') + this_q = Question(bot, channel, None, 'int', 45, embed=embed) + resp = await this_q.ask([ctx.author]) + + if not resp: + return None + if resp < count: + return matches[resp - 1] + else: + raise ValueError(f'{resp} is not a valid response.') + + +def get_player_positions(player): + """ + :param player: Player instance + :return list: positions (ex: ['1B', '3B'] or ['SP', 'RP', 'CP']) + """ + positions = [] + if player['pos_1']: + positions.append(player['pos_1']) + if player['pos_2']: + positions.append(player['pos_2']) + if player['pos_3']: + positions.append(player['pos_3']) + if player['pos_4']: + positions.append(player['pos_4']) + if player['pos_5']: + positions.append(player['pos_5']) + if player['pos_6']: + positions.append(player['pos_6']) + if player['pos_7']: + positions.append(player['pos_7']) + if player['pos_8']: + positions.append(player['pos_8']) + + return positions + + +def get_team_embed(title, team=None, thumbnail: bool = True): + if team: + embed = discord.Embed( + title=title, + color=int(team["color"], 16) if team["color"] and int(team["color"], 16) <= 16777215 + else int(SBA_COLOR, 16) + ) + if thumbnail: + if 'thumbnail' in team: + embed.set_thumbnail(url=team["thumbnail"] if team["thumbnail"] else LOGO) + elif 'logo' in team: + embed.set_thumbnail(url=team["logo"] if team["logo"] else LOGO) + embed.set_footer(text=f'SBa Season {team["season"]}', icon_url=LOGO) + else: + embed = discord.Embed( + title=title, + color=int(SBA_COLOR, 16) + ) + if thumbnail: + embed.set_thumbnail(url=LOGO) + embed.set_footer(text=f'SBa Season 6', icon_url=LOGO) + return embed + + +async def get_or_create_role(ctx, role_name, mentionable=True): + this_role = discord.utils.get(ctx.guild.roles, name=role_name) + logging.info(f'this_role: {this_role} / role_name: {role_name} (POST SEARCH)') + + if not this_role: + this_role = await ctx.guild.create_role(name=role_name, mentionable=mentionable) + logging.info(f'this_role: {this_role} / role_name: {role_name} (PRE RETURN)') + + return this_role + + +def get_role(ctx, role_name, bot=None): + role = None + + if not ctx: + if bot: + guild = bot.get_guild(int(os.environ.get('GUILD_ID'))) + if not guild: + logging.error('Cannot send to channel - bot not logged in') + return + role = discord.utils.get(guild.roles, name=role_name) + else: + role = discord.utils.get(ctx.guild.roles, name=role_name) + + logging.info(f'this_role: {role} / role_name: {role_name} (PRE RETURN)') + if role: + return role + else: + return None + + +def get_team_role(ctx, team, bot=None): + return get_role(ctx, team['lname'], bot) + + +async def send_to_channel(bot, channel_name, content=None, embed=None): + guild = bot.get_guild(int(os.environ.get('GUILD_ID'))) + if not guild: + logging.error('Cannot send to channel - bot not logged in') + return + + this_channel = discord.utils.get(guild.text_channels, name=channel_name) + + if not this_channel: + this_channel = discord.utils.get(guild.text_channels, id=channel_name) + if not this_channel: + raise NameError(f'**{channel_name}** channel not found') + + await this_channel.send(content=content, embed=embed) + + +async def get_player_embed(player, current, ctx=None): + player_name = player['name'] + if player['il_return']: + if player['team']['abbrev'][-2:].lower() == 'il': + player_name = f'🏥 {player_name}' + else: + player_name = f'{await get_emoji(ctx, "WeenieHut", False)}{player_name}' + embed = get_team_embed(f'{player_name}', player["team"]) + embed.set_footer(text=f'SBa Season {current["season"]}', icon_url=LOGO) + embed.add_field(name='Current Team', value=player['team']['sname']) + + if player['headshot']: + embed.set_thumbnail(url=player['headshot']) + elif player['vanity_card']: + embed.set_thumbnail(url=player['vanity_card']) + else: + player_photo = await get_player_headshot(player['name']) + if player_photo: + embed.set_thumbnail(url=player_photo) + + player_trans = await get_transactions( + current['season'], + week_start=current['week'], + week_end=current['week'] + 1, + player_id=player['id'] + ) + + for x in player_trans: + if player_trans[x]['week'] == current['week']: + embed.add_field(name='Last Week', value=f'{player_trans[x]["oldteam"]["sname"]}') + if player_trans[x]['week'] == current['week'] + 1: + embed.add_field(name='Next Week', value=f'To {player_trans[x]["newteam"]["sname"]}') + + embed.add_field(name='sWAR', value=player['wara']) + embed.set_image(url=player['image']) + + player_pages = f'[SBa]({get_player_url(player)}) / ' \ + f'[BBRef]({get_player_url(player, "bbref")})' + embed.add_field(name='Player Page', value=player_pages) + positions = get_player_positions(player) + if len(positions) > 0: + embed.add_field(name=f'Position{"s" if len(positions) > 1 else ""}', value=",".join(positions)) + if player['team']['abbrev'][-3:].lower() == 'mil': + major_team = await get_one_team(player['team']['abbrev'][:-3], season=player['season']) + embed.add_field(name='SBa Affiliate', value=major_team['sname']) + if player['last_game']: + embed.add_field(name='Last G', value=player['last_game']) + if player['last_game2']: + embed.add_field(name='Last G-2', value=player['last_game2']) + if player['il_return']: + embed.add_field(name='IL Return', value=player['il_return']) + if player['injury_rating'] is not None: + inj_string = f'{player["injury_rating"]}' + if player['pos_1'] in ['SP', 'RP']: + inj_code = player['injury_rating'][:1] + inj_string += f'(6-{13 - int(inj_code)})' + embed.add_field(name='Injury', value=inj_string) + if player['pitcher_injury'] is not None: + if player['pitcher_injury']: + embed.add_field(name='P Injury', value=f'{player["pitcher_injury"]} (6-{13 - player["pitcher_injury"]})') + else: + embed.add_field(name='P Injury', value=f'{player["pitcher_injury"]} (---)') + if player['demotion_week'] is not None: + if player['demotion_week'] > current['week']: + embed.add_field(name='Dem Week', value=player["demotion_week"]) + + player_awards = await get_awards(season=current['season'], player_name=player['name']) + awards = [] + + if len(player_awards) > 0: + for x in player_awards: + awards.append(player_awards[x]['name']) + award_string = ', '.join(awards[-3:]) + if len(awards) > 3: + award_string += f', plus {len(awards) - 3} more' + embed.add_field(name='Awards', value=award_string, inline=False) + + b = await get_one_battingseason(player['id']) + p = await get_one_pitchingseason(player['id']) + + batting_string = None + pitching_string = None + + if len(b) > 0: + if b['ab'] > 0: + singles = b['hit'] - b['hr'] - b['triple'] - b['double'] + avg = b['hit'] / b['ab'] + obp = (b['hit'] + b['bb'] + b['ibb'] + b['hbp']) / b['pa'] + slg = ((b['hr'] * 4) + (b['triple'] * 3) + (b['double'] * 2) + singles) / b['ab'] + ops = obp + slg + woba = ((b['bb'] * .69) + (b['hbp'] * .72) + (singles * .89) + (b['double'] * 1.27) + + (b['triple'] * 1.62) + (b['hr'] * 2.1)) / (b['pa'] - b['hbp'] - b['sac']) + ab = f'{b["ab"]:.0f}' + run = f'{b["run"]:.0f}' + hit = f'{b["hit"]:.0f}' + double = f'{b["double"]:.0f}' + triple = f'{b["triple"]:.0f}' + hr = f'{b["hr"]:.0f}' + rbi = f'{b["rbi"]:.0f}' + sb = f'{b["sb"]:.0f}' + cs = f'{b["cs"]:.0f}' + so = f'{b["so"]:.0f}' + + batting_string = f'```\n' \ + f' AVG OBP SLG OPS\n' \ + f' {avg:.3f} {obp:.3f} {slg:.3f} {ops:.3f}\n``````\n' \ + f' AB R H 2B 3B HR RBI SB SO\n' \ + f'{ab: >3} {run: >2} {hit: ^3} {double: >2} {triple: >2} {hr: >2} {rbi: >3} ' \ + f'{sb: >2} {so: ^3}\n```' + + if len(p) > 0: + if p['ip'] > 0: + win = f'{p["win"]:.0f}' + loss = f'{p["loss"]:.0f}' + save = f'{p["sv"]:.0f}' + era = f'{(p["erun"] * 9) / p["ip"]:.2f}' + game = f'{p["game"]:.0f}' + gs = f'{p["gs"]:.0f}' + ip = f'{p["ip"]:.0f}' + if p["ip"] % 1 == 0: + ip += '.0' + elif str(p["ip"] % 1)[2] == '3': + ip += '.1' + else: + ip += '.2' + so = f'{p["so"]:.0f}' + whip = f'{(p["bb"] + p["hit"]) / p["ip"]:.2f}' + pitching_string = f'```\n' \ + f' W L SV ERA IP SO WHIP\n' \ + f'{win: >2} {loss: >2} {save: >2} {era: >5} {ip: >5} ' \ + f'{so: >3} {whip: >4}\n```' + + if batting_string and len(b) > len(p): + embed.add_field(name='Batting Stats', value=batting_string, inline=False) + if pitching_string: + embed.add_field(name='Pitching Stats', value=pitching_string, inline=False) + if batting_string and len(p) >= len(b): + embed.add_field(name='Batting Stats', value=batting_string, inline=False) + + return embed + + +def get_player_url(player, which="sba"): + stub_name = player["name"].replace(" ", "%20") + if which == 'bbref': + return f'https://www.baseball-reference.com/search/search.fcgi?search={stub_name}' + else: + return f'https://sombaseball.ddns.net/players?name={stub_name}' + + +def get_channel(ctx, name): + channel = discord.utils.get( + ctx.guild.text_channels, + name=name + ) + if channel: + return channel + + return None + + +async def create_channel( + ctx, channel_name: str, category_name: str, everyone_send: bool = False, everyone_read: bool = True, + read_send_members: list = None, read_send_roles: list = None, read_only_roles: list = None, + read_only_pokemon: bool = True): + this_category = discord.utils.get(ctx.guild.categories, name=category_name) + if not this_category: + raise ValueError(f'I couldn\'t find a category named **{category_name}**') + + overwrites = { + ctx.guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True), + ctx.guild.default_role: discord.PermissionOverwrite(read_messages=everyone_read, send_messages=everyone_send) + } + if read_send_members: + for member in read_send_members: + overwrites[member] = discord.PermissionOverwrite(read_messages=True, send_messages=True) + if read_send_roles: + for role in read_send_roles: + overwrites[role] = discord.PermissionOverwrite(read_messages=True, send_messages=True) + if read_only_roles: + for role in read_only_roles: + overwrites[role] = discord.PermissionOverwrite(read_messages=True, send_messages=False) + if read_only_pokemon: + poke_role = get_role(ctx, 'Pokétwo') + overwrites[poke_role] = discord.PermissionOverwrite(read_messages=True, send_messages=False) + + this_channel = await ctx.guild.create_text_channel( + channel_name, + overwrites=overwrites, + category=this_category + ) + + logging.info(f'Creating channel ({channel_name}) in ({category_name})') + + return this_channel + + +async def get_major_team(team): + if 'Il' in team['abbrev'][len(team['abbrev']) - 2:].lower(): + return await get_one_team(team['abbrev'][:-2]) + else: + return team + + +async def react_and_reply(ctx, reaction, message): + await ctx.message.add_reaction(reaction) + await ctx.send(message) + + +async def toggle_draft_sheet() -> None: + sheets = pygsheets.authorize(service_file='storage/major-domo-service-creds.json') + this_sheet = sheets.open_by_key(SBA_SEASON6_DRAFT_KEY) + my_cards = this_sheet.worksheet_by_title('BUTTON') + logging.info(f'Toggling the draft button...') + + my_cards.update_value( + 'B3', 'FALSE' + ) + await asyncio.sleep(1) + my_cards.update_value( + 'B3', 'TRUE' + ) + + +async def log_injury(current: dict, inj_dict: dict) -> None: + sheets = pygsheets.authorize(service_file='storage/major-domo-service-creds.json') + this_sheet = sheets.open_by_key(SBA_ROSTER_KEY) + injury_sheet = this_sheet.worksheet_by_title('Injury Log') + + logging.info(f'updating values') + + try: + injury_sheet.update_values( + crange=f'A{current["injury_count"]+2}', + values=[[ + datetime.datetime.now().strftime('%Y-%m-%d, %H:%M:%S'), inj_dict['player']['name'], inj_dict['type'], + inj_dict['player']['team']['abbrev'], current['week'], inj_dict['current_game'], + inj_dict['injury_length'] + ]] + ) + except Exception as e: + logging.error(f'log_injury sheets: {e}') + raise Exception(e) + + +def get_pos_abbrev(pos_name): + if pos_name == 'Catcher': + return 'C' + elif pos_name == 'First Base': + return '1B' + elif pos_name == 'Second Base': + return '2B' + elif pos_name == 'Third Base': + return '3B' + elif pos_name == 'Shortstop': + return 'SS' + elif pos_name == 'Left Field': + return 'LF' + elif pos_name == 'Center Field': + return 'CF' + elif pos_name == 'Right Field': + return 'RF' + elif pos_name == 'Pitcher': + return 'P' + elif pos_name == 'Designated Hitter': + return 'DH' + elif pos_name == 'Pinch Hitter': + return 'PH' + else: + raise KeyError(f'{pos_name} is not a recognized position name') + + +def new_rand_conf_gif(): + req_url = 'https://api.giphy.com/v1/gifs/translate?s=all done&api_key=H86xibttEuUcslgmMM6uu74IgLEZ7UOD' + + resp = requests.get(req_url, timeout=3) + if resp.status_code == 200: + data = resp.json() + if 'trump' in data['data']['title']: + return random_conf_gif() + else: + return data['data']['url'] + else: + logging.warning(resp.text) + raise ValueError(f'DB: {resp.text}') diff --git a/majordomo.py b/majordomo.py new file mode 100644 index 0000000..2fa7972 --- /dev/null +++ b/majordomo.py @@ -0,0 +1,58 @@ +import asyncio +import datetime +import logging +import os + +import discord +from discord.ext import commands + +date = f'{datetime.datetime.now().year}-{datetime.datetime.now().month}-{datetime.datetime.now().day}' +log_level = logging.INFO if os.environ.get('LOG_LEVEL') == 'INFO' else 'WARN' +logging.basicConfig( + filename=f'logs/{date}.log', + format='%(asctime)s - majordomo - %(levelname)s - %(message)s', + level=log_level +) + +COGS = [ + 'cogs.owner', + 'cogs.players', + 'cogs.transactions', + 'cogs.admins', + 'cogs.dice', + 'cogs.fun', + # 'cogs.gameday', + # 'cogs.gameplay' +] + +intents = discord.Intents.default() +intents.message_content = True +intents.members = True +bot = commands.Bot( + command_prefix='!', + intents=intents, + description='The Strat-o-matic Bot\nIf you have questions, feel free to contact Cal.', + case_insensitive=True, + owner_id=258104532423147520 +) + + +@bot.event +async def on_ready(): + logging.info('Logged in as:') + logging.info(bot.user.name) + logging.info(bot.user.id) + + +async def main(): + for c in COGS: + try: + await bot.load_extension(c) + logging.info(f'Loaded cog: {c}') + except Exception as e: + logging.error(f'Failed to load cog: {c}') + logging.error(f'{e}') + async with bot: + await bot.start(os.environ.get('BOT_TOKEN')) + +asyncio.run(main()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d4e8c3d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +discord.py @ git+https://github.com/Rapptz/discord.py +requests +pydantic +pygsheets +peewee +bs4 +pandas