Multiple fixes to resolve PlayNotFoundException and lineup initialization errors: 1. gauntlets.py: - Fixed Team object subscriptable errors (use .id instead of ['id']) - Added fallback cardsets (24, 25, 26) for Event 9 RP shortage - Fixed draft_team type handling (can be Team object or dict) 2. cogs/gameplay.py: - Fixed gauntlet game creation flow to read field player lineup from sheets - Catches LineupsMissingException when SP not yet selected - Instructs user to run /gamestate after SP selection 3. utilities/dropdown.py: - Fixed SelectStartingPitcher to create own session instead of using closed session - Store game/team IDs instead of objects to avoid detached session issues - Added exception handling for failed legality check API calls These changes fix the issue where gauntlet games would fail to initialize because the SP lineup entry wasn't being committed to the database. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1490 lines
74 KiB
Python
1490 lines
74 KiB
Python
import asyncio
|
|
import logging
|
|
import os
|
|
from typing import Literal
|
|
|
|
import discord
|
|
from discord import app_commands
|
|
from discord import SelectOption
|
|
from discord.app_commands import Choice
|
|
from discord.ext import commands, tasks
|
|
import pygsheets
|
|
|
|
import sqlalchemy
|
|
from sqlmodel import func, or_
|
|
|
|
from api_calls import db_get
|
|
from command_logic.logic_gameplay import bunts, chaos, complete_game, defender_dropdown_view, doubles, flyballs, frame_checks, get_full_roster_from_sheets, checks_log_interaction, complete_play, get_scorebug_embed, groundballs, hit_by_pitch, homeruns, is_game_over, lineouts, manual_end_game, new_game_checks, new_game_conflicts, popouts, read_lineup, relief_pitcher_dropdown_view, select_ai_reliever, show_defense_cards, singles, starting_pitcher_dropdown_view, steals, strikeouts, sub_batter_dropdown_view, substitute_player, triples, undo_play, update_game_settings, walks, xchecks, activate_last_play
|
|
from dice import ab_roll
|
|
from exceptions import *
|
|
import gauntlets
|
|
from helpers import CARDSETS, DEFENSE_LITERAL, DEFENSE_NO_PITCHER_LITERAL, PD_PLAYERS_ROLE_NAME, SELECT_CARDSET_OPTIONS, Dropdown, get_channel, send_to_channel, team_role, user_has_role, random_gif, random_from_list
|
|
|
|
# from in_game import ai_manager
|
|
from in_game.ai_manager import get_starting_pitcher, get_starting_lineup
|
|
from in_game.game_helpers import PUBLIC_FIELDS_CATEGORY_NAME, legal_check
|
|
from in_game.gameplay_models import GameCardsetLink, Lineup, Play, Session, engine, player_description, select, Game
|
|
from in_game.gameplay_queries import get_all_positions, get_cardset_or_none, get_one_lineup, get_plays_by_pitcher, get_position, get_channel_game_or_none, get_active_games_by_team, get_game_lineups, get_team_or_none
|
|
|
|
from utilities.buttons import Confirm, ScorebugButtons, ask_confirm, ask_with_buttons
|
|
from utilities.dropdown import DropdownView
|
|
|
|
|
|
logger = logging.getLogger('discord_app')
|
|
CLASSIC_EMBED = True
|
|
CARDSETS
|
|
|
|
|
|
class Gameplay(commands.Cog):
|
|
def __init__(self, bot):
|
|
self.bot = bot
|
|
self.sheets = None
|
|
self.game_states = {} # game_id: {play: <Current Play>, ack: <bool>}
|
|
|
|
self.get_sheets.start()
|
|
self.live_scorecard.start()
|
|
|
|
@tasks.loop(count=1)
|
|
async def get_sheets(self):
|
|
logger.info(f'Getting sheets')
|
|
self.sheets = pygsheets.authorize(service_file='storage/paper-dynasty-service-creds.json', retries=1)
|
|
|
|
@tasks.loop(minutes=1)
|
|
async def live_scorecard(self):
|
|
try:
|
|
logger.info(f'Checking live scorecard loop')
|
|
|
|
guild = self.bot.get_guild(int(os.environ.get('GUILD_ID')))
|
|
score_channel = discord.utils.get(guild.text_channels, name='live-pd-scores')
|
|
|
|
if score_channel is None:
|
|
logger.error(f'Could not find live-pd-channel')
|
|
return
|
|
|
|
if len(self.game_states) == 0:
|
|
logger.info(f'No active game_states')
|
|
return
|
|
|
|
player_role = discord.utils.get(guild.roles, name=PD_PLAYERS_ROLE_NAME)
|
|
all_embeds = []
|
|
logger.info(f'player role: {player_role}')
|
|
|
|
with Session(engine) as session:
|
|
for key in self.game_states:
|
|
if not self.game_states[key]['ack']:
|
|
this_game = session.get(Game, key)
|
|
if this_game is None:
|
|
log_exception(GameNotFoundException, f'Could not pull game #{key} for live scorecard')
|
|
|
|
if not this_game.active:
|
|
logger.info(f'Game {this_game.id} is complete, removing from game_states')
|
|
del self.game_states[key]
|
|
|
|
else:
|
|
try:
|
|
logger.info(f'Appending scorebug for Game {this_game.id}')
|
|
this_channel = discord.utils.get(guild.text_channels, id=this_game.channel_id)
|
|
logger.info(f'this_channel: {this_channel}')
|
|
|
|
this_embed = await get_scorebug_embed(session, this_game, full_length=False, live_scorecard=True)
|
|
this_embed.set_image(url=None)
|
|
this_embed.insert_field_at(
|
|
index=0,
|
|
name='Ballpark',
|
|
value=f'{this_channel.mention}'
|
|
)
|
|
all_embeds.append(this_embed)
|
|
|
|
self.game_states[key]['ack'] = True
|
|
except Exception as e:
|
|
logger.error(f'Unable to add to game_states: {e}')
|
|
logger.error(f'Game: {this_game.id}')
|
|
|
|
if len(all_embeds) == 0:
|
|
logger.info(f'No active game embeds, returning')
|
|
await score_channel.set_permissions(player_role, read_messages=False)
|
|
return
|
|
|
|
async for message in score_channel.history(limit=25):
|
|
await message.delete()
|
|
|
|
await score_channel.set_permissions(player_role, read_messages=True)
|
|
await score_channel.send(content=None, embeds=all_embeds)
|
|
except Exception as e:
|
|
logger.error(f'Failed running live scorecard: {e}')
|
|
# try:
|
|
# await send_to_channel(self.bot, 'commissioners-office', f'PD Live Scorecard just failed: {e}')
|
|
# except Exception as e:
|
|
# logger.error(f'Couldn\'t even send the error to the private channel :/')
|
|
|
|
@live_scorecard.before_loop
|
|
async def before_live_scoreboard(self):
|
|
await self.bot.wait_until_ready()
|
|
|
|
@get_sheets.before_loop
|
|
async def before_get_sheets(self):
|
|
logger.info(f'Waiting to get sheets')
|
|
await self.bot.wait_until_ready()
|
|
|
|
async def cog_command_error(self, ctx, error):
|
|
logger.error(msg=error, stack_info=True)
|
|
await ctx.send(f'{error}\n\nRun !help <command_name> to see the command requirements')
|
|
|
|
async def slash_error(self, ctx, error):
|
|
logger.error(msg=error, stack_info=True)
|
|
await ctx.send(f'{error[:1600]}')
|
|
|
|
async def post_play(self, session: Session, interaction: discord.Interaction, this_play: Play, buffer_message: str = None, full_length: bool = False):
|
|
logger.info(f'post_play - Posting new play: {this_play}')
|
|
|
|
if this_play is None:
|
|
logger.info(f'this_play is None, searching for game in channel {interaction.channel.id}')
|
|
this_game = get_channel_game_or_none(session, interaction.channel.id)
|
|
try:
|
|
this_play = activate_last_play(session, this_game)
|
|
except Exception as e:
|
|
this_play = this_game.initialize_play(session)
|
|
finally:
|
|
if this_play is None:
|
|
log_exception(PlayNotFoundException, f'Attempting to display gamestate, but cannot find current play')
|
|
|
|
if is_game_over(this_play):
|
|
logger.info(f'Game {this_play.game.id} seems to be over')
|
|
await interaction.edit_original_response(content=f'Looks like this one is over!')
|
|
submit_game = await ask_confirm(
|
|
interaction=interaction,
|
|
question=f'Final score: {this_play.game.away_team.abbrev} {this_play.away_score} - {this_play.home_score} {this_play.game.home_team.abbrev}\n{this_play.scorebug_ascii}\nShould I go ahead and submit this game or roll it back a play?',
|
|
custom_confirm_label='Submit',
|
|
custom_cancel_label='Roll Back'
|
|
)
|
|
|
|
if submit_game:
|
|
logger.info(f'post_play - is_game_over - {interaction.user.display_name} rejected game completion')
|
|
await complete_game(session, interaction, this_play, self.bot)
|
|
return
|
|
else:
|
|
logger.warning(f'post_play - is_game_over - {interaction.user.display_name} rejected game completion in Game {this_play.game.id}')
|
|
|
|
cal_channel = get_channel(interaction, 'commissioners-office')
|
|
await cal_channel.send(content=f'{interaction.user.display_name} just rejected game completion down in {interaction.channel.mention}')
|
|
|
|
this_play = undo_play(session, this_play)
|
|
|
|
await self.post_play(session, interaction, this_play)
|
|
await interaction.channel.send(content=f'I let Cal know his bot is stupid')
|
|
|
|
if this_play.pitcher.is_fatigued and not this_play.ai_is_batting:
|
|
if this_play.managerai.replace_pitcher(session, this_play.game):
|
|
logger.info(f'Running a pitcher sub')
|
|
await interaction.edit_original_response(content='The AI is making a pitching change...')
|
|
new_pitcher_card = await select_ai_reliever(session, this_play.pitcher.team, this_play)
|
|
new_pitcher_lineup = substitute_player(session, this_play, this_play.pitcher, new_pitcher_card, 'P')
|
|
logger.info(f'Sub complete')
|
|
|
|
scorebug_buttons, this_ab_roll = None, None
|
|
scorebug_embed = await get_scorebug_embed(session, this_play.game, full_length=full_length, classic=CLASSIC_EMBED)
|
|
|
|
if this_play.game.roll_buttons and interaction.user.id in [this_play.game.away_team.gmid, this_play.game.home_team.gmid]:
|
|
logger.info(f'Including scorebug buttons')
|
|
scorebug_buttons = ScorebugButtons(this_play, scorebug_embed, timeout=8)
|
|
|
|
if this_play.on_base_code == 0 and this_play.game.auto_roll and not this_play.batter.team.is_ai and not this_play.is_new_inning:
|
|
logger.info(f'Rolling ab')
|
|
this_ab_roll = ab_roll(this_play.batter.team, this_play.game, allow_chaos=False)
|
|
scorebug_buttons = None
|
|
|
|
if this_ab_roll is not None and this_ab_roll.d_six_one > 3:
|
|
logger.info(f'Setting embed image to pitcher')
|
|
scorebug_embed.set_image(url=this_play.pitcher.player.pitcher_card_url)
|
|
|
|
if buffer_message is not None:
|
|
logger.info(f'Posting buffered message')
|
|
await interaction.edit_original_response(
|
|
content=buffer_message
|
|
)
|
|
sb_message = await interaction.channel.send(
|
|
content=None,
|
|
embed=scorebug_embed,
|
|
view=scorebug_buttons
|
|
)
|
|
else:
|
|
logger.info(f'Posting unbuffered message')
|
|
sb_message = await interaction.edit_original_response(
|
|
content=None,
|
|
embed=scorebug_embed,
|
|
view=scorebug_buttons
|
|
)
|
|
|
|
if this_ab_roll is not None:
|
|
logger.info(f'Posting ab roll')
|
|
await interaction.channel.send(
|
|
content=None,
|
|
embeds=this_ab_roll.embeds
|
|
)
|
|
|
|
if scorebug_buttons is not None:
|
|
logger.info(f'Posting scorebug buttons roll')
|
|
await scorebug_buttons.wait()
|
|
if not scorebug_buttons.value:
|
|
await sb_message.edit(view=None)
|
|
|
|
async def complete_and_post_play(self, session: Session, interaction: discord.Interaction, this_play: Play, buffer_message: str = None):
|
|
next_play = complete_play(session, this_play)
|
|
logger.info(f'Completed play {this_play.id}')
|
|
|
|
logger.info(f'Updating self.game_states')
|
|
self.game_states[this_play.game.id] = {'play': this_play, 'ack': False}
|
|
logger.info(f'New state: {self.game_states}')
|
|
|
|
await self.post_play(session, interaction, next_play, buffer_message)
|
|
|
|
def kickstart_live_scorecard(self):
|
|
try:
|
|
self.live_scorecard.start()
|
|
logger.info(f'Kick started the live scorecard')
|
|
except RuntimeError as e:
|
|
logger.info(f'Live scorecard is already running')
|
|
|
|
@commands.command(name='test-write', help='Test concurrent db writes', hidden=True)
|
|
@commands.is_owner()
|
|
async def test_write_command(self, ctx):
|
|
await ctx.send(f'I am going to open a connection, delay, then try to write')
|
|
with Session(engine) as session:
|
|
ncb_team = await get_team_or_none(session, team_id=31)
|
|
await ctx.send(f'The {ncb_team.lname} has_guide value is: {ncb_team.has_guide}. Now to delay for 10 seconds...')
|
|
ncb_team.has_guide = not ncb_team.has_guide
|
|
session.add(ncb_team)
|
|
await asyncio.sleep(10)
|
|
await ctx.send(f'Now to attempt committing the NCB change...')
|
|
session.commit()
|
|
await ctx.send(f'Am I alive? Did it work?')
|
|
|
|
group_new_game = app_commands.Group(name='new-game', description='Start a new baseball game')
|
|
|
|
@group_new_game.command(name='mlb-campaign', description='Start a new MLB campaign game against an AI')
|
|
@app_commands.choices(
|
|
league=[
|
|
Choice(value='minor-league', name='Minor League'),
|
|
Choice(value='flashback', name='Flashback'),
|
|
Choice(value='major-league', name='Major League'),
|
|
Choice(value='hall-of-fame', name='Hall of Fame')
|
|
],
|
|
roster=[
|
|
Choice(value='1', name='Primary'),
|
|
Choice(value='2', name='Secondary'),
|
|
Choice(value='3', name='Ranked')
|
|
]
|
|
)
|
|
@app_commands.checks.has_any_role(PD_PLAYERS_ROLE_NAME)
|
|
async def new_game_mlb_campaign_command(
|
|
self, interaction: discord.Interaction, league: Choice[str], away_team_abbrev: str, home_team_abbrev: str, roster: Choice[str]):
|
|
await interaction.response.defer()
|
|
self.kickstart_live_scorecard()
|
|
|
|
with Session(engine) as session:
|
|
teams = await new_game_checks(session, interaction, away_team_abbrev, home_team_abbrev)
|
|
if teams is None:
|
|
logger.error(f'Received None from new_game_checks, cancelling new game')
|
|
return
|
|
|
|
away_team = teams['away_team']
|
|
home_team = teams['home_team']
|
|
|
|
ai_team = away_team if away_team.is_ai else home_team
|
|
human_team = away_team if home_team.is_ai else home_team
|
|
|
|
|
|
conflict_games = get_active_games_by_team(session, team=human_team)
|
|
if len(conflict_games) > 0:
|
|
await interaction.edit_original_response(
|
|
content=f'Ope. The {human_team.sname} are already playing over in {interaction.guild.get_channel(conflict_games[0].channel_id).mention}'
|
|
)
|
|
return
|
|
|
|
|
|
conflict_games = get_active_games_by_team(session, team=human_team)
|
|
if len(conflict_games) > 0:
|
|
await interaction.edit_original_response(
|
|
content=f'Ope. The {human_team.sname} are already playing over in {interaction.guild.get_channel(conflict_games[0].channel_id).mention}'
|
|
)
|
|
return
|
|
|
|
current = await db_get('current')
|
|
week_num = current['week']
|
|
logger.info(f'gameplay - new_game_mlb_campaign - Season: {current["season"]} / Week: {week_num} / Away Team: {away_team.description} / Home Team: {home_team.description}')
|
|
|
|
def role_error(required_role: str, league_name: str, lower_league: str):
|
|
return f'Ope. Looks like you haven\'t received the **{required_role}** role, yet!\n\nTo play **{league_name}** games, you need to defeat all 30 MLB teams in the {lower_league} campaign. You can see your progress with `/record`.\n\nIf you have completed the {lower_league} campaign, go ping Cal to get your new role!'
|
|
|
|
if league.value == 'flashback':
|
|
if not user_has_role(interaction.user, 'PD - Major League'):
|
|
await interaction.edit_original_response(
|
|
content=role_error('PD - Major League', league_name='Flashback', lower_league='Minor League')
|
|
)
|
|
return
|
|
elif league.value == 'major-league':
|
|
if not user_has_role(interaction.user, 'PD - Major League'):
|
|
await interaction.edit_original_response(
|
|
content=role_error('PD - Major League', league_name='Major League', lower_league='Minor League')
|
|
)
|
|
return
|
|
elif league.value == 'hall-of-fame':
|
|
if not user_has_role(interaction.user, 'PD - Hall of Fame'):
|
|
await interaction.edit_original_response(
|
|
content=role_error('PD - Hall of Fame', league_name='Hall of Fame', lower_league='Major League')
|
|
)
|
|
return
|
|
|
|
this_game = Game(
|
|
away_team_id=away_team.id,
|
|
home_team_id=home_team.id,
|
|
away_roster_id=69 if away_team.is_ai else int(roster.value),
|
|
home_roster_id=69 if home_team.is_ai else int(roster.value),
|
|
channel_id=interaction.channel_id,
|
|
season=current['season'],
|
|
week=week_num,
|
|
first_message=None if interaction.message is None else interaction.message.channel.id,
|
|
ai_team='away' if away_team.is_ai else 'home',
|
|
game_type=league.value
|
|
)
|
|
|
|
game_info_log = f'{league.name} game between {away_team.description} and {home_team.description} / first message: {this_game.first_message}'
|
|
logger.info(game_info_log)
|
|
|
|
# Get AI SP
|
|
await interaction.edit_original_response(
|
|
content=f'{ai_team.gmname} is looking for a Starting Pitcher...'
|
|
)
|
|
ai_sp_lineup = await get_starting_pitcher(
|
|
session,
|
|
ai_team,
|
|
this_game,
|
|
True if home_team.is_ai else False,
|
|
league.value
|
|
)
|
|
logger.info(f'Chosen SP in Game {this_game.id}: {ai_sp_lineup.player.name_with_desc}')
|
|
await interaction.edit_original_response(
|
|
content=f'The {ai_team.sname} are starting **{ai_sp_lineup.player.name_with_desc}**:\n\n{ai_sp_lineup.player.pitcher_card_url}'
|
|
)
|
|
|
|
# Get AI Lineup
|
|
final_message = await interaction.channel.send(
|
|
content=f'{ai_team.gmname} is filling out the {ai_team.sname} lineup card...'
|
|
)
|
|
logger.info(f'Pulling lineup...')
|
|
batter_lineups = await get_starting_lineup(
|
|
session,
|
|
team=ai_team,
|
|
game=this_game,
|
|
league_name=this_game.league_name,
|
|
sp_name=ai_sp_lineup.player.name
|
|
)
|
|
|
|
# Check for last game settings
|
|
logger.info(f'Checking human team\'s automation preferences...')
|
|
g_query = session.exec(select(Game).where(or_(Game.home_team == human_team, Game.away_team == human_team)).order_by(Game.id.desc()).limit(1)).all()
|
|
|
|
if len(g_query) > 0:
|
|
last_game = g_query[0]
|
|
this_game.auto_roll = last_game.auto_roll
|
|
this_game.roll_buttons = last_game.roll_buttons
|
|
logger.info(f'Setting auto_roll to {last_game.auto_roll} and roll_buttons to {last_game.roll_buttons}')
|
|
|
|
# Commit game and lineups
|
|
session.add(this_game)
|
|
session.commit()
|
|
|
|
await final_message.edit(content=f'The {ai_team.sname} lineup is in, pulling in scouting data...')
|
|
for batter in batter_lineups:
|
|
|
|
pos_count = await get_all_positions(
|
|
session=session,
|
|
this_card=batter.card
|
|
)
|
|
if pos_count != 0:
|
|
logger.info(f'logged position ratings for {batter.player.name_with_desc}')
|
|
else:
|
|
logger.warning(f'received no positions for {batter.player.name_with_desc}')
|
|
if batter.position not in ['P', 'DH']:
|
|
log_exception(PositionNotFoundException, f'{batter.player.name_with_desc} is listed at {batter.position} but no ratings were found.')
|
|
|
|
logger.info(f'Pulling team roles')
|
|
away_role = await team_role(interaction, this_game.away_team)
|
|
home_role = await team_role(interaction, this_game.home_team)
|
|
|
|
logger.info(f'Building scorebug embed')
|
|
embed = await get_scorebug_embed(session, this_game)
|
|
embed.clear_fields()
|
|
embed.add_field(
|
|
name=f'{ai_team.abbrev} Lineup',
|
|
value=this_game.team_lineup(session, ai_team)
|
|
)
|
|
|
|
logger.info(f'Pulling and caching full {human_team.abbrev} roster')
|
|
done = await get_full_roster_from_sheets(session, interaction, self.sheets, this_game, human_team, int(roster.value))
|
|
|
|
roster_choice = await ask_with_buttons(
|
|
interaction,
|
|
['vs Left', 'vs Right'],
|
|
'Which lineup will you be using?',
|
|
delete_question=False,
|
|
confirmation_message='Got it!'
|
|
)
|
|
|
|
sp_view = starting_pitcher_dropdown_view(session, this_game, human_team, this_game.league_name, [interaction.user])
|
|
await interaction.channel.send(content=f'### {human_team.lname} Starting Pitcher', view=sp_view)
|
|
|
|
try:
|
|
await asyncio.sleep(5)
|
|
this_play = await read_lineup(
|
|
session,
|
|
interaction,
|
|
this_game=this_game,
|
|
lineup_team=human_team,
|
|
sheets_auth=self.sheets,
|
|
lineup_num=1 if roster_choice == 'vs Right' else 2,
|
|
league_name=this_game.game_type
|
|
)
|
|
except LineupsMissingException as e:
|
|
logger.error(f'Attempting to start game, pausing for 5 seconds: {e}')
|
|
await asyncio.sleep(5)
|
|
|
|
try:
|
|
this_play = this_game.current_play_or_none(session)
|
|
await self.post_play(session, interaction, this_play, buffer_message='Game on!')
|
|
except LineupsMissingException as e:
|
|
await interaction.channel.send(
|
|
content=f'Run `/gamestate` once you have selected a Starting Pitcher to get going!'
|
|
)
|
|
|
|
@group_new_game.command(name='gauntlet', description='Start a new Gauntlet game against an AI')
|
|
@app_commands.choices(
|
|
roster=[
|
|
Choice(value='1', name='Primary'),
|
|
Choice(value='2', name='Secondary'),
|
|
Choice(value='3', name='Ranked')
|
|
]
|
|
)
|
|
@app_commands.checks.has_any_role(PD_PLAYERS_ROLE_NAME)
|
|
async def new_game_gauntlet_command(self, interaction: discord.Interaction, roster: Choice[str]):
|
|
await interaction.response.defer()
|
|
self.kickstart_live_scorecard()
|
|
|
|
with Session(engine) as session:
|
|
try:
|
|
await new_game_conflicts(session, interaction)
|
|
except GameException as e:
|
|
return
|
|
|
|
main_team = await get_team_or_none(
|
|
session,
|
|
gm_id=interaction.user.id,
|
|
main_team=True
|
|
)
|
|
human_team = await get_team_or_none(
|
|
session,
|
|
gm_id=interaction.user.id,
|
|
gauntlet_team=True
|
|
)
|
|
|
|
if not main_team:
|
|
await interaction.edit_original_response(
|
|
content=f'I don\'t see a team for you, yet. You can sign up with the `/newteam` command!'
|
|
)
|
|
return
|
|
|
|
if not human_team:
|
|
await interaction.edit_original_response(
|
|
content=f'I don\'t see an active run for you. You can get started with the `/gauntlets start` command!'
|
|
)
|
|
return
|
|
|
|
e_query = await db_get('events', params=[('active', True)])
|
|
if e_query['count'] == 0:
|
|
await interaction.edit_original_response(
|
|
content=f'Hm. It looks like there aren\'t any active gauntlets. What do we even pay Cal for?'
|
|
)
|
|
return
|
|
|
|
elif e_query['count'] == 1:
|
|
this_event = e_query['events'][0]
|
|
r_query = await db_get(
|
|
'gauntletruns',
|
|
params=[('team_id', human_team.id), ('gauntlet_id', this_event['id']), ('is_active', True)]
|
|
)
|
|
|
|
if r_query['count'] == 0:
|
|
await interaction.edit_original_response(
|
|
content=f'I don\'t see an active run for you. If you would like to start a new one, run '
|
|
f'`/gauntlets start {this_event["name"]}` and we can get you started in no time!'
|
|
)
|
|
return
|
|
|
|
this_run = r_query['runs'][0]
|
|
|
|
else:
|
|
r_query = await db_get(
|
|
'gauntletruns',
|
|
params=[('team_id', human_team.id), ('is_active', True)]
|
|
)
|
|
|
|
if r_query['count'] == 0:
|
|
await interaction.edit_original_response(
|
|
content=f'I don\'t see an active run for you. If you would like to start a new one, run '
|
|
f'`/gauntlets start {e_query["events"][0]["name"]}` and we can get you started in no time!'
|
|
)
|
|
return
|
|
else:
|
|
this_run = r_query['runs'][0]
|
|
this_event = r_query['runs'][0]['gauntlet']
|
|
|
|
# If not new or after draft, create new AI game
|
|
is_home = gauntlets.is_home_team(human_team, this_event, this_run)
|
|
ai_team = await gauntlets.get_opponent(session, human_team, this_event, this_run)
|
|
if ai_team is None:
|
|
await interaction.edit_original_response(
|
|
content=f'Yike. I\'m not sure who your next opponent is. Plz ping the shit out of Cal!'
|
|
)
|
|
return
|
|
else:
|
|
logger.info(f'opponent: {ai_team}')
|
|
|
|
ai_role = await team_role(interaction, ai_team)
|
|
human_role = await team_role(interaction, main_team)
|
|
away_role = ai_role if is_home else human_role
|
|
home_role = human_role if is_home else ai_role
|
|
|
|
conflict_games = get_active_games_by_team(session, team=human_team)
|
|
if len(conflict_games) > 0:
|
|
await interaction.edit_original_response(
|
|
content=f'Ope. The {human_team.sname} are already playing over in {interaction.guild.get_channel(conflict_games[0].channel_id).mention}'
|
|
)
|
|
return
|
|
|
|
current = await db_get('current')
|
|
game_code = gauntlets.get_game_code(human_team, this_event, this_run)
|
|
|
|
this_game = Game(
|
|
away_team_id=ai_team.id if is_home else human_team.id,
|
|
home_team_id=human_team.id if is_home else ai_team.id,
|
|
channel_id=interaction.channel_id,
|
|
season=current['season'],
|
|
week=current['week'],
|
|
first_message=None if interaction.message is None else interaction.message.id,
|
|
ai_team='away' if is_home else 'home',
|
|
away_roster_id=69 if is_home else int(roster.value),
|
|
home_roster_id=int(roster.value) if is_home else 69,
|
|
game_type=game_code
|
|
)
|
|
logger.info(
|
|
f'Game between {human_team.abbrev} and {ai_team.abbrev} is initializing!'
|
|
)
|
|
|
|
# Get AI SP
|
|
await interaction.edit_original_response(
|
|
content=f'{ai_team.gmname} is looking for a Starting Pitcher...'
|
|
)
|
|
ai_sp_lineup = await gauntlets.get_starting_pitcher(
|
|
session,
|
|
ai_team,
|
|
this_game,
|
|
this_event,
|
|
this_run
|
|
)
|
|
logger.info(f'Chosen SP in Game {this_game.id}: {ai_sp_lineup.player.name_with_desc}')
|
|
await interaction.edit_original_response(
|
|
content=f'The {ai_team.sname} are starting **{ai_sp_lineup.player.name_with_desc}**:\n\n{ai_sp_lineup.player.pitcher_card_url}'
|
|
)
|
|
|
|
# Get AI Lineup
|
|
final_message = await interaction.channel.send(
|
|
content=f'{ai_team.gmname} is filling out the {ai_team.sname} lineup card...'
|
|
)
|
|
logger.info(f'Pulling lineup in Game {this_game.id}')
|
|
batter_lineups = await get_starting_lineup(
|
|
session,
|
|
team=ai_team,
|
|
game=this_game,
|
|
league_name=f'gauntlet-{this_event["id"]}',
|
|
sp_name=ai_sp_lineup.player.name
|
|
)
|
|
|
|
# Check for last game settings
|
|
logger.info(f'Checking human team\'s automation preferences...')
|
|
g_query = session.exec(select(Game).where(or_(Game.home_team == human_team, Game.away_team == human_team)).order_by(Game.id.desc()).limit(1)).all()
|
|
|
|
if len(g_query) > 0:
|
|
last_game = g_query[0]
|
|
this_game.auto_roll = last_game.auto_roll
|
|
this_game.roll_buttons = last_game.roll_buttons
|
|
logger.info(f'Setting auto_roll to {last_game.auto_roll} and roll_buttons to {last_game.roll_buttons}')
|
|
|
|
# Commit game and lineups
|
|
session.add(this_game)
|
|
session.commit()
|
|
|
|
await final_message.edit(content=f'The {ai_team.sname} lineup is in, pulling in scouting data...')
|
|
for batter in batter_lineups:
|
|
await get_all_positions(
|
|
session=session,
|
|
this_card=batter.card
|
|
)
|
|
|
|
embed = await get_scorebug_embed(session, this_game)
|
|
embed.clear_fields()
|
|
embed.add_field(
|
|
name=f'{ai_team.abbrev} Lineup',
|
|
value=this_game.team_lineup(session, ai_team)
|
|
)
|
|
|
|
# Get pitchers from rosterlinks
|
|
done = await get_full_roster_from_sheets(session, interaction, self.sheets, this_game, human_team, 1)
|
|
# if done:
|
|
# sp_view = starting_pitcher_dropdown_view(session, this_game, human_team, game_type=this_game.league_name, responders=[interaction.user])
|
|
# sp_message = await interaction.channel.send(content=f'### {human_team.lname} Starting Pitcher', view=sp_view)
|
|
|
|
# await final_message.edit(
|
|
# content=f'{away_role.mention} @ {home_role.mention} is set!',
|
|
# embed=embed
|
|
# )
|
|
|
|
roster_choice = await ask_with_buttons(
|
|
interaction,
|
|
['vs Left', 'vs Right'],
|
|
'Which lineup will you be using?',
|
|
delete_question=False,
|
|
confirmation_message='Got it!'
|
|
)
|
|
|
|
# Read the 9 field players from sheets (this will fail to initialize play without SP)
|
|
try:
|
|
await read_lineup(
|
|
session,
|
|
interaction,
|
|
this_game=this_game,
|
|
lineup_team=human_team,
|
|
sheets_auth=self.sheets,
|
|
lineup_num=1 if roster_choice == 'vs Right' else 2,
|
|
league_name=this_game.game_type
|
|
)
|
|
except LineupsMissingException as e:
|
|
# Expected - can't initialize play without SP yet
|
|
logger.info(f'Field player lineup read from sheets, waiting for SP selection: {e}')
|
|
|
|
sp_view = starting_pitcher_dropdown_view(session, this_game, human_team, this_game.league_name, [interaction.user])
|
|
await interaction.channel.send(content=f'### {human_team.lname} Starting Pitcher', view=sp_view)
|
|
|
|
# Don't try to initialize play immediately - wait for user to select SP
|
|
# The play will be initialized when they run /gamestate
|
|
await interaction.channel.send(
|
|
content=f'Once you\'ve selected your Starting Pitcher, run `/gamestate` to get the game started!'
|
|
)
|
|
|
|
@group_new_game.command(name='exhibition', description='Start a new custom game against an AI')
|
|
@app_commands.choices(
|
|
roster=[
|
|
Choice(value='1', name='Primary'),
|
|
Choice(value='2', name='Secondary'),
|
|
Choice(value='3', name='Ranked')
|
|
]
|
|
)
|
|
@app_commands.checks.has_any_role(PD_PLAYERS_ROLE_NAME)
|
|
async def new_game_exhibition_command(self, interaction: discord.Interaction, away_team_abbrev: str, home_team_abbrev: str, roster: Choice[str], cardsets: Literal['Minor League', 'Major League', 'Hall of Fame', 'Flashback', 'Custom'] = 'Custom'):
|
|
await interaction.response.defer()
|
|
self.kickstart_live_scorecard()
|
|
|
|
with Session(engine) as session:
|
|
teams = await new_game_checks(session, interaction, away_team_abbrev, home_team_abbrev)
|
|
if teams is None:
|
|
logger.error(f'Received None from new_game_checks, cancelling new game')
|
|
return
|
|
|
|
away_team = teams['away_team']
|
|
home_team = teams['home_team']
|
|
|
|
if not away_team.is_ai ^ home_team.is_ai:
|
|
await interaction.edit_original_response(
|
|
content=f'I don\'t see an AI team in this Exhibition game. Run `/new-game exhibition` again with an AI for a custom game or `/new-game <ranked / unlimited>` for a PvP game.'
|
|
)
|
|
return
|
|
|
|
current = await db_get('current')
|
|
week_num = current['week']
|
|
logger.info(f'gameplay - new_game_mlb_campaign - Season: {current["season"]} / Week: {week_num} / Away Team: {away_team.description} / Home Team: {home_team.description}')
|
|
|
|
ai_team = away_team if away_team.is_ai else home_team
|
|
human_team = away_team if home_team.is_ai else home_team
|
|
|
|
this_game = Game(
|
|
away_team_id=away_team.id,
|
|
home_team_id=home_team.id,
|
|
away_roster_id=69 if away_team.is_ai else int(roster.value),
|
|
home_roster_id=69 if home_team.is_ai else int(roster.value),
|
|
channel_id=interaction.channel_id,
|
|
season=current['season'],
|
|
week=week_num,
|
|
first_message=None if interaction.message is None else interaction.message.id,
|
|
ai_team='away' if away_team.is_ai else 'home',
|
|
game_type='exhibition'
|
|
)
|
|
|
|
async def new_game_setup():
|
|
# Get AI SP
|
|
await interaction.edit_original_response(
|
|
content=f'{ai_team.gmname} is looking for a Starting Pitcher...'
|
|
)
|
|
ai_sp_lineup = await get_starting_pitcher(
|
|
session,
|
|
ai_team,
|
|
this_game,
|
|
True if home_team.is_ai else False,
|
|
'exhibition'
|
|
)
|
|
logger.info(f'Chosen SP: {ai_sp_lineup.player.name_with_desc}')
|
|
session.add(ai_sp_lineup)
|
|
await interaction.edit_original_response(
|
|
content=f'The {ai_team.sname} are starting **{ai_sp_lineup.player.name_with_desc}**:\n\n{ai_sp_lineup.player.pitcher_card_url}'
|
|
)
|
|
|
|
# Get AI Lineup
|
|
final_message = await interaction.channel.send(
|
|
content=f'{ai_team.gmname} is filling out the {ai_team.sname} lineup card...'
|
|
)
|
|
logger.info(f'Pulling lineup...')
|
|
batter_lineups = await get_starting_lineup(
|
|
session,
|
|
team=ai_team,
|
|
game=this_game,
|
|
league_name=this_game.league_name,
|
|
sp_name=ai_sp_lineup.player.name
|
|
)
|
|
for x in batter_lineups:
|
|
session.add(x)
|
|
|
|
# Check for last game settings
|
|
logger.info(f'Checking human team\'s automation preferences...')
|
|
g_query = session.exec(select(Game).where(or_(Game.home_team == human_team, Game.away_team == human_team)).order_by(Game.id.desc()).limit(1)).all()
|
|
|
|
if len(g_query) > 0:
|
|
last_game = g_query[0]
|
|
this_game.auto_roll = last_game.auto_roll
|
|
this_game.roll_buttons = last_game.roll_buttons
|
|
logger.info(f'Setting auto_roll to {last_game.auto_roll} and roll_buttons to {last_game.roll_buttons}')
|
|
|
|
# Commit game and lineups
|
|
session.add(this_game)
|
|
session.commit()
|
|
|
|
await final_message.edit(content=f'The {ai_team.sname} lineup is in, pulling in scouting data...')
|
|
for batter in batter_lineups:
|
|
await get_all_positions(
|
|
session=session,
|
|
this_card=batter.card
|
|
)
|
|
|
|
logger.info(f'Pulling team roles')
|
|
away_role = await team_role(interaction, this_game.away_team)
|
|
home_role = await team_role(interaction, this_game.home_team)
|
|
|
|
logger.info(f'Building scorebug embed')
|
|
embed = await get_scorebug_embed(session, this_game)
|
|
embed.clear_fields()
|
|
embed.add_field(
|
|
name=f'{ai_team.abbrev} Lineup',
|
|
value=this_game.team_lineup(session, ai_team)
|
|
)
|
|
|
|
logger.info(f'Pulling and caching full {human_team.abbrev} roster')
|
|
done = await get_full_roster_from_sheets(session, interaction, self.sheets, this_game, human_team, int(roster.value))
|
|
if done:
|
|
sp_view = starting_pitcher_dropdown_view(session, this_game, human_team, this_game.league_name, [interaction.user])
|
|
await interaction.channel.send(content=f'### {human_team.lname} Starting Pitcher', view=sp_view)
|
|
|
|
await final_message.edit(
|
|
content=f'{away_role.mention} @ {home_role.mention} is set!\n\n'
|
|
f'Go ahead and set your lineup with the `/set lineup` command!',
|
|
embed=embed
|
|
)
|
|
|
|
if cardsets != 'Custom':
|
|
c_list = CARDSETS[cardsets]
|
|
for row in c_list['primary']:
|
|
this_cardset = await get_cardset_or_none(session, cardset_id=row)
|
|
if this_cardset is not None:
|
|
this_link = GameCardsetLink(
|
|
game=this_game,
|
|
cardset=this_cardset,
|
|
priority=1
|
|
)
|
|
session.add(this_link)
|
|
|
|
for row in c_list['secondary']:
|
|
this_cardset = await get_cardset_or_none(session, cardset_id=row)
|
|
this_link = GameCardsetLink(
|
|
game=this_game,
|
|
cardset=this_cardset,
|
|
priority=2
|
|
)
|
|
|
|
await new_game_setup()
|
|
|
|
else:
|
|
async def my_callback(interaction: discord.Interaction, values):
|
|
logger.info(f'Setting custom cardsets inside callback')
|
|
await interaction.response.defer(thinking=True)
|
|
logger.info(f'values: {values}')
|
|
for cardset_id in values:
|
|
logger.info(f'Getting cardset: {cardset_id}')
|
|
this_cardset = await get_cardset_or_none(session, cardset_id)
|
|
logger.info(f'this_cardset: {this_cardset}')
|
|
this_link = GameCardsetLink(
|
|
game=this_game,
|
|
cardset=this_cardset,
|
|
priority=1
|
|
)
|
|
session.add(this_link)
|
|
|
|
logger.info(f'Done processing links')
|
|
session.commit()
|
|
await interaction.edit_original_response(content="Got it...")
|
|
|
|
await new_game_setup()
|
|
|
|
my_dropdown = Dropdown(
|
|
option_list=SELECT_CARDSET_OPTIONS,
|
|
placeholder='Select up to 8 cardsets to include',
|
|
callback=my_callback,
|
|
max_values=len(SELECT_CARDSET_OPTIONS)
|
|
)
|
|
view = DropdownView([my_dropdown])
|
|
await interaction.edit_original_response(
|
|
content=None,
|
|
view=view
|
|
)
|
|
|
|
# TODO: add new-game ranked
|
|
@group_new_game.command(name='unlimited', description='Start a new Unlimited game against another human')
|
|
@app_commands.checks.has_any_role(PD_PLAYERS_ROLE_NAME)
|
|
async def new_game_unlimited_command(self, interaction: discord.Interaction, away_team_abbrev: str, home_team_abbrev: str):
|
|
await interaction.response.defer()
|
|
self.kickstart_live_scorecard()
|
|
|
|
with Session(engine) as session:
|
|
teams = await new_game_checks(session, interaction, away_team_abbrev, home_team_abbrev)
|
|
if teams is None:
|
|
logger.error(f'Received None from new_game_checks, cancelling new game')
|
|
return
|
|
|
|
away_team = teams['away_team']
|
|
home_team = teams['home_team']
|
|
|
|
if away_team.is_ai or home_team.is_ai:
|
|
await interaction.edit_original_response(
|
|
content=f'Unlimited games are for two human-run teams. To play against the AI, you can play `mlb-campaign`, `gauntlet`, or `exhibition` game modes.'
|
|
)
|
|
return
|
|
|
|
current = await db_get('current')
|
|
week_num = current['week']
|
|
logger.info(f'gameplay - new_game_unlimited - Season: {current["season"]} / Week: {week_num} / Away Team: {away_team.description} / Home Team: {home_team.description}')
|
|
|
|
this_game = Game(
|
|
away_team_id=away_team.id,
|
|
home_team_id=home_team.id,
|
|
away_roster_id=None,
|
|
home_roster_id=None,
|
|
channel_id=interaction.channel_id,
|
|
season=current['season'],
|
|
week=week_num,
|
|
first_message=None if interaction.message is None else interaction.message.id,
|
|
game_type='exhibition'
|
|
)
|
|
|
|
await interaction.edit_original_response(
|
|
content=f'Let\'s get set up for **{away_team.lname}** @ **{home_team.lname}**!'
|
|
)
|
|
|
|
away_role = await team_role(interaction, away_team)
|
|
home_role = await team_role(interaction, home_team)
|
|
|
|
away_roster_id = await ask_with_buttons(
|
|
interaction=interaction,
|
|
button_options=[
|
|
'Primary', 'Secondary', 'Ranked'
|
|
],
|
|
question=f'{away_role.mention}\nWhich roster should I pull for you?',
|
|
delete_question=False,
|
|
confirmation_message=f'Got it! As soon as the {home_team.sname} select their roster, I will pull them both in at once.'
|
|
)
|
|
|
|
home_roster_id = await ask_with_buttons(
|
|
interaction=interaction,
|
|
button_options=[
|
|
'Primary', 'Secondary', 'Ranked'
|
|
],
|
|
question=f'{home_role.mention}\nWhich roster should I pull for you?',
|
|
delete_question=False,
|
|
confirmation_message=f'Got it! Off to Sheets I go for the {away_team.abbrev} roster...'
|
|
)
|
|
|
|
if away_roster_id and home_roster_id:
|
|
if away_roster_id == 'Primary':
|
|
away_roster_id = 1
|
|
elif away_roster_id == 'Secondary':
|
|
away_roster_id = 2
|
|
else:
|
|
away_roster_id = 3
|
|
|
|
if home_roster_id == 'Primary':
|
|
home_roster_id = 1
|
|
elif home_roster_id == 'Secondary':
|
|
home_roster_id = 2
|
|
else:
|
|
home_roster_id = 3
|
|
|
|
logger.info(f'Setting roster IDs - away: {away_roster_id} / home: {home_roster_id}')
|
|
this_game.away_roster_id = away_roster_id
|
|
this_game.home_roster_id = home_roster_id
|
|
session.add(this_game)
|
|
session.commit()
|
|
|
|
logger.info(f'Pulling away team\'s roster')
|
|
away_roster = await get_full_roster_from_sheets(session, interaction, self.sheets, this_game, away_team, away_roster_id)
|
|
|
|
# if away_roster:
|
|
logger.info(f'Pulling home team\'s roster')
|
|
await interaction.channel.send(
|
|
content=f'And now for the {home_team.abbrev} sheet...'
|
|
)
|
|
home_roster = await get_full_roster_from_sheets(session, interaction, self.sheets, this_game, home_team,home_roster_id)
|
|
|
|
# if home_roster:
|
|
await interaction.channel.send(
|
|
content=f'{away_role.mention} @ {home_role.mention}\n\nThe game is set, both of you may run `/set <starting-pitcher and lineup>` to start!'
|
|
)
|
|
|
|
|
|
@commands.command(name='force-endgame', help='Mod: Force a game to end without stats')
|
|
async def force_end_game_command(self, ctx: commands.Context):
|
|
with Session(engine) as session:
|
|
this_game = get_channel_game_or_none(session, ctx.channel.id)
|
|
|
|
if this_game is None:
|
|
await ctx.send(f'I do not see a game here - are you in the right place?')
|
|
return
|
|
|
|
try:
|
|
await ctx.send(
|
|
content=None,
|
|
embed=await get_scorebug_embed(session, this_game, full_length=True)
|
|
)
|
|
except Exception as e:
|
|
logger.error(f'Unable to display scorebug while forcing game to end: {e}')
|
|
await ctx.send(content='This game is so boned that I can\'t display the scorebug.')
|
|
|
|
nuke_game = await ask_confirm(
|
|
ctx,
|
|
question=f'Is this the game I should nuke?',
|
|
label_type='yes',
|
|
timeout=15,
|
|
)
|
|
|
|
# if view.value:
|
|
if nuke_game:
|
|
this_game.active = False
|
|
session.add(this_game)
|
|
session.commit()
|
|
await ctx.channel.send(content=random_gif(random_from_list(['i killed it', 'deed is done', 'gone forever'])))
|
|
else:
|
|
await ctx.send(f'It stays. For now.')
|
|
|
|
group_set_rosters = app_commands.Group(name='set', description='Set SP and lineup')
|
|
|
|
@group_set_rosters.command(name='lineup', description='Import a saved lineup for this channel\'s PD game.')
|
|
@app_commands.describe(
|
|
lineup='Which handedness lineup are you using?'
|
|
)
|
|
@app_commands.choices(
|
|
lineup=[
|
|
Choice(value='1', name='v Right'),
|
|
Choice(value='2', name='v Left')
|
|
]
|
|
)
|
|
@app_commands.checks.has_any_role(PD_PLAYERS_ROLE_NAME)
|
|
async def read_lineup_command(self, interaction: discord.Interaction, lineup: Choice[str]):
|
|
await interaction.response.defer()
|
|
|
|
with Session(engine) as session:
|
|
this_game = get_channel_game_or_none(session, interaction.channel_id)
|
|
if this_game is None:
|
|
await interaction.edit_original_response(
|
|
content=f'Hm. I don\'t see a game going on in this channel. Am I drunk?'
|
|
)
|
|
return
|
|
|
|
if this_game.away_team.gmid == interaction.user.id:
|
|
this_team = this_game.away_team
|
|
elif this_game.home_team.gmid == interaction.user.id:
|
|
this_team = this_game.home_team
|
|
else:
|
|
logger.info(f'{interaction.user.name} tried to run a command in Game {this_game.id} when they aren\'t a GM in the game.')
|
|
await interaction.edit_original_response(content='Bruh. Only GMs of the active teams can pull lineups.')
|
|
return
|
|
|
|
all_lineups = get_game_lineups(session, this_game, this_team)
|
|
if len(all_lineups) > 1:
|
|
play_count = session.exec(select(func.count(Play.id)).where(Play.game == this_game, Play.complete == True)).one()
|
|
if play_count > 0:
|
|
await interaction.edit_original_response(
|
|
content=f'Since {play_count} play{"s" if play_count != 1 else ""} ha{"ve" if play_count != 1 else "s"} been logged, you will have to run `/substitution batter` to replace any of your batters.'
|
|
)
|
|
return
|
|
|
|
logger.info(f'lineup: {lineup} / value: {lineup.value} / name: {lineup.name}')
|
|
try:
|
|
this_play = await read_lineup(
|
|
session,
|
|
interaction,
|
|
this_game=this_game,
|
|
lineup_team=this_team,
|
|
sheets_auth=self.sheets,
|
|
lineup_num=int(lineup.value),
|
|
league_name=this_game.game_type
|
|
)
|
|
except LineupsMissingException as e:
|
|
await interaction.edit_original_response(content='Run `/set starting-pitcher` to select your SP')
|
|
return
|
|
|
|
if this_play is not None:
|
|
await self.post_play(session, interaction, this_play)
|
|
|
|
@group_set_rosters.command(name='starting-pitcher')
|
|
@app_commands.checks.has_any_role(PD_PLAYERS_ROLE_NAME)
|
|
async def set_starting_pitcher(self, interaction: discord.Interaction):
|
|
await interaction.response.defer()
|
|
|
|
with Session(engine) as session:
|
|
this_game = get_channel_game_or_none(session, interaction.channel_id)
|
|
if this_game is None:
|
|
await interaction.edit_original_response(
|
|
content=f'Hm. I don\'t see a game going on in this channel. Am I drunk?'
|
|
)
|
|
return
|
|
|
|
if this_game.away_team.gmid == interaction.user.id:
|
|
this_team = this_game.away_team
|
|
elif this_game.home_team.gmid == interaction.user.id:
|
|
this_team = this_game.home_team
|
|
else:
|
|
logger.info(f'{interaction.user.name} tried to run a command in Game {this_game.id} when they aren\'t a GM in the game.')
|
|
await interaction.edit_original_response(content='Bruh. Only GMs of the active teams can pull lineups.')
|
|
return
|
|
|
|
try:
|
|
check_sp = get_one_lineup(session, this_game, this_team, position='P')
|
|
play_count = session.exec(select(func.count(Play.id)).where(Play.game == this_game, Play.complete == True, Play.pitcher == check_sp)).one()
|
|
if play_count > 0:
|
|
await interaction.edit_original_response(
|
|
content=f'Since {play_count} play{"s" if play_count != 1 else ""} ha{"ve" if play_count != 1 else "s"} been logged, you will have to run `/substitution pitcher` to replace {check_sp.player.name}.'
|
|
)
|
|
return
|
|
|
|
except sqlalchemy.exc.NoResultFound as e:
|
|
# if 'NoResultFound' not in str(e):
|
|
# logger.error(f'Error checking for existing sp: {e}')
|
|
# log_exception(e, 'Unable to check your lineup for an existing SP')
|
|
# else:
|
|
logger.info(f'No pitcher in game, good to go')
|
|
check_sp = None
|
|
|
|
if check_sp is not None:
|
|
logger.info(f'Already an SP in Game {this_game.id}, asking if we should swap')
|
|
swap_sp = await ask_confirm(
|
|
interaction,
|
|
question=f'{check_sp.player.name} is already scheduled to start this game - would you like to switch?',
|
|
label_type='yes'
|
|
)
|
|
if not swap_sp:
|
|
logger.info(f'No swap being made')
|
|
await interaction.edit_original_response(content=f'We will leave {check_sp.player.name} on the lineup card.')
|
|
return
|
|
|
|
session.delete(check_sp)
|
|
session.commit()
|
|
|
|
sp_view = starting_pitcher_dropdown_view(session, this_game, this_team, game_type=this_game.league_name, responders=[interaction.user])
|
|
await interaction.edit_original_response(content=f'### {this_team.lname} Starting Pitcher', view=sp_view)
|
|
|
|
@app_commands.command(name='gamestate', description='Post the current game state')
|
|
async def gamestate_command(self, interaction: discord.Interaction, include_lineups: bool = False):
|
|
await interaction.response.defer(ephemeral=True, thinking=True)
|
|
|
|
with Session(engine) as session:
|
|
this_game = get_channel_game_or_none(session, interaction.channel_id)
|
|
if this_game is None:
|
|
await interaction.edit_original_response(
|
|
content=f'Hm. I don\'t see a game going on in this channel. Am I drunk?'
|
|
)
|
|
return
|
|
|
|
this_play = this_game.current_play_or_none(session)
|
|
try:
|
|
await self.post_play(session, interaction, this_play, full_length=include_lineups, buffer_message=None if this_game.human_team.gmid != interaction.user.id else 'Posting current play')
|
|
except LineupsMissingException as e:
|
|
logger.info(f'Could not post full scorebug embed, posting lineups')
|
|
ai_team = this_game.away_team if this_game.ai_team == 'away' else this_game.home_team
|
|
embed = await get_scorebug_embed(session, this_game)
|
|
embed.clear_fields()
|
|
embed.add_field(
|
|
name=f'{ai_team.abbrev} Lineup',
|
|
value=this_game.team_lineup(session, ai_team)
|
|
)
|
|
|
|
@app_commands.command(name='settings-ingame', description='Change in-game settings')
|
|
@app_commands.describe(
|
|
roll_buttons='Display the "Roll AB" and "Check Jump" buttons along with the scorebug',
|
|
auto_roll='When there are no baserunners, automatically roll the next AB'
|
|
)
|
|
async def game_settings_command(self, interaction: discord.Interaction, roll_buttons: bool = None, auto_roll: bool = None):
|
|
with Session(engine) as session:
|
|
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='settings-ingame')
|
|
|
|
await interaction.edit_original_response(content=None, embed=await update_game_settings(
|
|
session,
|
|
interaction,
|
|
this_game,
|
|
roll_buttons=roll_buttons,
|
|
auto_roll=auto_roll
|
|
))
|
|
|
|
@app_commands.command(name='end-game', description='End the current game in this channel')
|
|
@app_commands.checks.has_any_role(PD_PLAYERS_ROLE_NAME)
|
|
async def end_game_command(self, interaction: discord.Interaction):
|
|
with Session(engine) as session:
|
|
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='end-game')
|
|
|
|
# await interaction.edit_original_response(content='Let\'s see, I didn\'t think this game was over...')
|
|
await manual_end_game(session, interaction, this_game, current_play=this_play)
|
|
|
|
group_substitution = app_commands.Group(name='substitute', description='Make a substitution in active game')
|
|
|
|
@group_substitution.command(name='batter', description='Make a batter substitution')
|
|
async def sub_batter_command(self, interaction: discord.Interaction, batting_order: Literal['this-spot', '1', '2', '3', '4', '5', '6', '7', '8', '9'] = 'this-spot'):
|
|
with Session(engine) as session:
|
|
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='substitute batter')
|
|
|
|
if batting_order == 'this-spot':
|
|
if this_play.batter.team != owner_team:
|
|
logger.info(f'Batting order not included while on defense; returning')
|
|
|
|
await interaction.edit_original_response(content=f'When you make a defensive substitution, please include the batting order where they should enter.')
|
|
return
|
|
this_order = this_play.batting_order
|
|
else:
|
|
this_order = int(batting_order)
|
|
|
|
logger.info(f'sub batter - this_play: {this_play}')
|
|
bat_view = sub_batter_dropdown_view(session, this_game, owner_team, this_order, [interaction.user])
|
|
await interaction.edit_original_response(content=f'### {owner_team.lname} Substitution', view=bat_view)
|
|
|
|
@group_substitution.command(name='pitcher', description='Make a pitching substitution')
|
|
async def sub_pitcher_command(self, interaction: discord.Interaction, batting_order: Literal['dh-spot', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] = '10'):
|
|
with Session(engine) as session:
|
|
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='substitute batter')
|
|
|
|
if owner_team != this_play.pitcher.team:
|
|
logger.warning(f'User {interaction.user.name} ({owner_team.abbrev}) tried to run a sub for the {this_play.pitcher.team.lname}')
|
|
await interaction.edit_original_response(
|
|
content=f'Please run pitcher subs when your team is on defense. If you are pinch-hitting for a pitcher already in the lineup, use `/substitute batter`'
|
|
)
|
|
return
|
|
|
|
if batting_order != '10' and this_play.pitcher.batting_order == 10:
|
|
forfeit_dh = await ask_confirm(
|
|
interaction,
|
|
f'Are you sure you want to forfeit the DH?'
|
|
)
|
|
if not forfeit_dh:
|
|
await interaction.edit_original_response(
|
|
content=f'Fine, be that way.'
|
|
)
|
|
return
|
|
|
|
if not this_play.is_new_inning:
|
|
pitcher_plays = get_plays_by_pitcher(session, this_game, this_play.pitcher)
|
|
batters_faced = sum(1 for x in pitcher_plays if x.pa == 1)
|
|
|
|
if batters_faced < 3:
|
|
await interaction.edit_original_response(
|
|
content=f'Looks like **{this_play.pitcher.player.name}** has only faced {batters_faced} of the 3-batter minimum.'
|
|
)
|
|
return
|
|
|
|
rp_view = relief_pitcher_dropdown_view(session, this_game, this_play.pitcher.team, batting_order, responders=[interaction.user])
|
|
rp_message = await interaction.edit_original_response(
|
|
content=f'### {this_play.pitcher.team.lname} Relief Pitcher',
|
|
view=rp_view
|
|
)
|
|
|
|
@group_substitution.command(name='defense', description='Make a defensive substitution or move defenders between positions')
|
|
async def sub_defense_command(self, interaction: discord.Interaction, new_position: DEFENSE_NO_PITCHER_LITERAL):
|
|
with Session(engine) as session:
|
|
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='substitute defense')
|
|
|
|
defense_view = await defender_dropdown_view(
|
|
session=session,
|
|
this_game=this_game,
|
|
human_team=owner_team,
|
|
new_position=new_position,
|
|
responders=[interaction.user]
|
|
)
|
|
defense_message = await interaction.edit_original_response(
|
|
content=f'### {owner_team.lname} {new_position} Change',
|
|
view=defense_view
|
|
)
|
|
|
|
group_log = app_commands.Group(name='log', description='Log a play in this channel\'s game')
|
|
|
|
@group_log.command(name='flyball', description='Flyballs: a, b, ballpark, bq, c')
|
|
async def log_flyball(self, interaction: discord.Interaction, flyball_type: Literal['a', 'b', 'ballpark', 'b?', 'c']):
|
|
with Session(engine) as session:
|
|
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log flyball')
|
|
|
|
logger.info(f'log flyball {flyball_type} - this_play: {this_play}')
|
|
this_play = await flyballs(session, interaction, this_play, flyball_type)
|
|
|
|
await self.complete_and_post_play(
|
|
session,
|
|
interaction,
|
|
this_play,
|
|
buffer_message='Flyball logged' if this_play.starting_outs + this_play.outs < 3 and ((this_play.on_second and flyball_type in ['b', 'ballpark']) or (this_play.on_third and flyball_type == 'b?')) else None
|
|
)
|
|
|
|
@group_log.command(name='frame-pitch', description=f'Walk/strikeout split; determined by home plate umpire')
|
|
async def log_frame_check(self, interaction: discord.Interaction):
|
|
with Session(engine) as session:
|
|
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log frame-check')
|
|
|
|
logger.info(f'log frame-check - this_play: {this_play}')
|
|
this_play = await frame_checks(session, interaction, this_play)
|
|
|
|
await self.complete_and_post_play(
|
|
session,
|
|
interaction,
|
|
this_play,
|
|
buffer_message='Frame check logged'
|
|
)
|
|
|
|
@group_log.command(name='lineout', description='Lineouts: one out, ballpark, max outs')
|
|
async def log_lineout(self, interaction: discord.Interaction, lineout_type: Literal['one-out', 'ballpark', 'max-outs']):
|
|
with Session(engine) as session:
|
|
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log lineout')
|
|
|
|
logger.info(f'log lineout - this_play: {this_play}')
|
|
this_play = await lineouts(session, interaction, this_play, lineout_type)
|
|
|
|
await self.complete_and_post_play(session, interaction, this_play, buffer_message='Lineout logged' if this_play.on_base_code > 3 else None)
|
|
|
|
@group_log.command(name='single', description='Singles: *, **, ballpark, uncapped')
|
|
async def log_single(
|
|
self, interaction: discord.Interaction, single_type: Literal['*', '**', 'ballpark', 'uncapped']):
|
|
with Session(engine) as session:
|
|
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log single')
|
|
|
|
logger.info(f'log single {single_type} - this_play: {this_play}')
|
|
this_play = await singles(session, interaction, this_play, single_type)
|
|
|
|
await self.complete_and_post_play(session, interaction, this_play, buffer_message='Single logged' if ((this_play.on_first or this_play.on_second) and single_type == 'uncapped') else None)
|
|
|
|
@group_log.command(name='double', description='Doubles: **, ***, uncapped')
|
|
async def log_double(self, interaction: discord.Interaction, double_type: Literal['**', '***', 'uncapped']):
|
|
with Session(engine) as session:
|
|
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log double')
|
|
|
|
logger.info(f'log double {double_type} - this_play: {this_play}')
|
|
this_play = await doubles(session, interaction, this_play, double_type)
|
|
|
|
await self.complete_and_post_play(session, interaction, this_play, buffer_message='Double logged' if (this_play.on_first and double_type == 'uncapped') else None)
|
|
|
|
@group_log.command(name='triple', description='Triples: no sub-types')
|
|
async def log_triple(self, interaction: discord.Interaction):
|
|
with Session(engine) as session:
|
|
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log triple')
|
|
|
|
logger.info(f'log triple - this_play: {this_play}')
|
|
this_play = await triples(session, interaction, this_play)
|
|
|
|
await self.complete_and_post_play(session, interaction, this_play)
|
|
|
|
@group_log.command(name='homerun', description='Home Runs: ballpark, no-doubt')
|
|
async def log_homerun(self, interaction: discord.Interaction, homerun_type: Literal['ballpark', 'no-doubt']):
|
|
with Session(engine) as session:
|
|
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log homerun')
|
|
|
|
logger.info(f'log homerun {homerun_type} - this_play: {this_play}')
|
|
this_play = await homeruns(session, interaction, this_play, homerun_type)
|
|
|
|
await self.complete_and_post_play(session, interaction, this_play)
|
|
|
|
@group_log.command(name='walk', description='Walks: unintentional (default), intentional')
|
|
async def log_walk(self, interaction: discord.Interaction, walk_type: Literal['unintentional', 'intentional'] = 'unintentional'):
|
|
with Session(engine) as session:
|
|
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log walk')
|
|
|
|
logger.info(f'log walk {walk_type} - this_play: {this_play}')
|
|
this_play = await walks(session, interaction, this_play, walk_type)
|
|
|
|
await self.complete_and_post_play(session, interaction, this_play)
|
|
|
|
@group_log.command(name='strikeout', description='Strikeout')
|
|
async def log_strikeout(self, interaction: discord.Interaction):
|
|
with Session(engine) as session:
|
|
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log strikeout')
|
|
|
|
logger.info(f'log strikeout - this_play: {this_play}')
|
|
this_play = await strikeouts(session, interaction, this_play)
|
|
|
|
await self.complete_and_post_play(session, interaction, this_play)
|
|
|
|
@group_log.command(name='popout', description='Popout')
|
|
async def log_popout(self, interaction: discord.Interaction):
|
|
with Session(engine) as session:
|
|
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log popout')
|
|
|
|
logger.info(f'log popout - this_play: {this_play}')
|
|
this_play = await popouts(session, interaction, this_play)
|
|
|
|
await self.complete_and_post_play(session, interaction, this_play)
|
|
|
|
@group_log.command(name='groundball', description='Groundballs: a, b, c')
|
|
async def log_groundball(self, interaction: discord.Interaction, groundball_type: Literal['a', 'b', 'c']):
|
|
with Session(engine) as session:
|
|
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name=f'log groundball {groundball_type}')
|
|
|
|
logger.info(f'log groundball {groundball_type} - this_play: {this_play}')
|
|
this_play = await groundballs(session, interaction, this_play, groundball_type)
|
|
|
|
await self.complete_and_post_play(session, interaction, this_play)
|
|
|
|
@group_log.command(name='hit-by-pitch', description='Hit by pitch: batter to first; runners advance if forced')
|
|
async def log_hit_by_pitch(self, interaction: discord.Interaction):
|
|
with Session(engine) as session:
|
|
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log hit-by-pitch')
|
|
|
|
logger.info(f'log hit-by-pitch - this_play: {this_play}')
|
|
this_play = await hit_by_pitch(session, interaction, this_play)
|
|
|
|
await self.complete_and_post_play(session, interaction, this_play)
|
|
|
|
@group_log.command(name='chaos', description='Chaos: wild-pitch, passed-ball, balk, pickoff')
|
|
async def log_chaos(self, interaction: discord.Interaction, chaos_type: Literal['wild-pitch', 'passed-ball', 'balk', 'pickoff']):
|
|
with Session(engine) as session:
|
|
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log hit-by-pitch')
|
|
|
|
if this_play.on_base_code == 0:
|
|
await interaction.edit_original_response(
|
|
content=f'There cannot be chaos when the bases are empty.'
|
|
)
|
|
return
|
|
|
|
logger.info(f'log chaos - this_play: {this_play}')
|
|
this_play = await chaos(session, interaction, this_play, chaos_type)
|
|
|
|
await self.complete_and_post_play(session, interaction, this_play)
|
|
|
|
@group_log.command(name='bunt', description='Bunts: sacrifice, bad, popout, double-play, defense')
|
|
async def log_sac_bunt(self, interaction: discord.Interaction, bunt_type: Literal['sacrifice', 'bad', 'popout', 'double-play', 'defense']):
|
|
with Session(engine) as session:
|
|
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log bunt')
|
|
|
|
if this_play.on_base_code == 0:
|
|
await interaction.edit_original_response(
|
|
content=f'You cannot bunt when the bases are empty.'
|
|
)
|
|
return
|
|
elif this_play.starting_outs == 2:
|
|
await interaction.edit_original_response(
|
|
content=f'You cannot bunt with two outs.'
|
|
)
|
|
return
|
|
|
|
logger.info(f'log bunt - this_play: {this_play}')
|
|
this_play = await bunts(session, interaction, this_play, bunt_type)
|
|
|
|
await self.complete_and_post_play(session, interaction, this_play)
|
|
|
|
@group_log.command(name='stealing', description='Running: stolen-base, caught-stealing')
|
|
@app_commands.describe(to_base='Base the runner is advancing to; 2 for 2nd, 3 for 3rd, 4 for Home')
|
|
async def log_stealing(self, interaction: discord.Interaction, running_type: Literal['stolen-base', 'caught-stealing', 'steal-plus-overthrow'], to_base: Literal[2, 3, 4]):
|
|
with Session(engine) as session:
|
|
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log stealing')
|
|
|
|
if (to_base == 2 and this_play.on_first is None) or (to_base == 3 and this_play.on_second is None) or (to_base == 4 and this_play.on_third is None):
|
|
logger.info(f'Illegal steal attempt')
|
|
await interaction.edit_original_response(
|
|
content=f'I don\'t see a runner there.'
|
|
)
|
|
return
|
|
|
|
if (to_base == 3 and this_play.on_third is not None) or (to_base == 2 and this_play.on_second is not None):
|
|
logger.info(f'Stealing runner is blocked')
|
|
if to_base == 3:
|
|
content = f'{this_play.on_second.player.name} is blocked by {this_play.on_third.player.name}'
|
|
else:
|
|
content = f'{this_play.on_first.player.name} is blocked by {this_play.on_second.player.name}'
|
|
|
|
await interaction.edit_original_response(
|
|
content=content
|
|
)
|
|
return
|
|
|
|
logger.info(f'log stealing - this_play: {this_play}')
|
|
this_play = await steals(session, interaction, this_play, running_type, to_base)
|
|
|
|
await self.complete_and_post_play(session, interaction, this_play)
|
|
|
|
@group_log.command(name='xcheck', description='Defender makes an x-check')
|
|
@app_commands.choices(position=[
|
|
Choice(name='Pitcher', value='P'),
|
|
Choice(name='Catcher', value='C'),
|
|
Choice(name='First Base', value='1B'),
|
|
Choice(name='Second Base', value='2B'),
|
|
Choice(name='Third Base', value='3B'),
|
|
Choice(name='Shortstop', value='SS'),
|
|
Choice(name='Left Field', value='LF'),
|
|
Choice(name='Center Field', value='CF'),
|
|
Choice(name='Right Field', value='RF'),
|
|
])
|
|
async def log_xcheck_command(self, interaction: discord.Interaction, position: Choice[str]):
|
|
with Session(engine) as session:
|
|
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log xcheck')
|
|
|
|
logger.info(f'log xcheck - this_play: {this_play}')
|
|
this_play = await xchecks(session, interaction, this_play, position.value)
|
|
|
|
await self.complete_and_post_play(session, interaction, this_play, buffer_message='X-Check logged')
|
|
|
|
|
|
@group_log.command(name='undo-play', description='Roll back most recent play from the log')
|
|
async def log_undo_play_command(self, interaction: discord.Interaction):
|
|
with Session(engine) as session:
|
|
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log undo-play')
|
|
|
|
logger.info(f'log undo-play - this_play: {this_play}')
|
|
this_play = undo_play(session, this_play)
|
|
|
|
await self.post_play(session, interaction, this_play)
|
|
|
|
group_show = app_commands.Group(name='show-card', description='Display the player card for an active player')
|
|
|
|
@group_show.command(name='defense', description='Display a defender\'s player card')
|
|
async def show_defense_command(self, interaction: discord.Interaction, position: DEFENSE_LITERAL):
|
|
with Session(engine) as session:
|
|
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='show-card defense')
|
|
|
|
logger.info(f'show-card defense - position: {position}')
|
|
await show_defense_cards(session, interaction, this_play, position)
|
|
|
|
|
|
async def setup(bot):
|
|
await bot.add_cog(Gameplay(bot)) |