CLAUDE: Fix gauntlet game creation and Event 9 issues

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>
This commit is contained in:
Cal Corum 2025-11-09 18:56:38 -06:00
parent 07195f9ad3
commit c043948238
3 changed files with 86 additions and 65 deletions

View File

@ -656,14 +656,11 @@ class Gameplay(commands.Cog):
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)
# Read the 9 field players from sheets (this will fail to initialize play without SP)
try:
await asyncio.sleep(5)
this_play = await read_lineup(
session,
interaction,
await read_lineup(
session,
interaction,
this_game=this_game,
lineup_team=human_team,
sheets_auth=self.sheets,
@ -671,16 +668,17 @@ class Gameplay(commands.Cog):
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!'
)
# 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(

View File

@ -423,9 +423,12 @@ async def run_draft(interaction: discord.Interaction, main_team: Team, this_even
if this_event['id'] in [1, 2]:
max_counts['MVP'] = 2
elif this_event['id'] in [5, 6, 8, 9]:
# Handle draft_team as either Team object or dict
dt_season = draft_team.season if isinstance(draft_team, Team) else draft_team['season']
dt_id = draft_team.id if isinstance(draft_team, Team) else draft_team['id']
g_query = await db_get(
'games',
params=[('season', draft_team.season), ('team1_id', draft_team.id), ('gauntlet_id', this_event['id'])]
params=[('season', dt_season), ('team1_id', dt_id), ('gauntlet_id', this_event['id'])]
)
if g_query['count'] > 4:
game_count = g_query['count']
@ -779,6 +782,14 @@ async def run_draft(interaction: discord.Interaction, main_team: Team, this_even
slot_params.extend(params)
p_query = await db_get('players/random', params=slot_params)
# Fallback for Event 9 RP shortage
if this_event['id'] == 9 and x == 'RP' and p_query['count'] < 3:
logger.warning(f'Low RP count ({p_query["count"]}) in Event 9, expanding cardsets to 24, 25, 26')
fallback_params = [p for p in slot_params if p[0] != 'cardset_id']
fallback_params.extend([('cardset_id', 24), ('cardset_id', 25), ('cardset_id', 26)])
p_query = await db_get('players/random', params=fallback_params)
logger.info(f'Fallback query returned {p_query["count"]} RP options')
if p_query['count'] > 0:
# test_player_list = ''
# for z in p_query['players']:
@ -1720,10 +1731,13 @@ async def run_draft(interaction: discord.Interaction, main_team: Team, this_even
raise KeyError(f'I gotta be honest - I shit the bed here and wasn\'t able to get you enough players to fill '
f'a team. I have to wipe this team, but please draft again after you tell Cal his bot sucks.')
# Handle draft_team as either Team object or dict
draft_team_id = draft_team.id if isinstance(draft_team, Team) else draft_team['id']
this_pack = await db_post(
'packs/one',
payload={
'team_id': draft_team.id,
'team_id': draft_team_id,
'pack_type_id': 2,
'open_time': datetime.datetime.timestamp(datetime.datetime.now()) * 1000
}
@ -1731,13 +1745,13 @@ async def run_draft(interaction: discord.Interaction, main_team: Team, this_even
await db_post(
'cards',
payload={'cards': [
{'player_id': x['player_id'], 'team_id': draft_team.id, 'pack_id': this_pack['id']} for x in all_players
{'player_id': x['player_id'], 'team_id': draft_team_id, 'pack_id': this_pack['id']} for x in all_players
]}
)
await db_post(
'gauntletruns',
payload={
'team_id': draft_team.id,
'team_id': draft_team_id,
'gauntlet_id': this_event['id']
}
)

View File

@ -110,13 +110,13 @@ class SelectViewDefense(discord.ui.Select):
class SelectStartingPitcher(discord.ui.Select):
def __init__(self, this_game: Game, this_team: Team, session: Session, league_name: str, custom_id: str = MISSING, placeholder: str | None = None, options: List[SelectOption] = ..., responders: list[discord.User] = None) -> None:
logger.info(f'Inside SelectStartingPitcher init function')
self.game = this_game
self.team = this_team
self.session = session
# Store IDs instead of objects to avoid session issues
self.game_id = this_game.id
self.team_id = this_team.id
self.league_name = league_name
self.responders = responders
super().__init__(custom_id=custom_id, placeholder=placeholder, options=options)
async def callback(self, interaction: discord.Interaction):
if self.responders is not None and interaction.user not in self.responders:
await interaction.response.send_message(
@ -127,52 +127,61 @@ class SelectStartingPitcher(discord.ui.Select):
await interaction.response.defer(thinking=True)
logger.info(f'SelectStartingPitcher - selection: {self.values[0]}')
# Get Human SP card
human_sp_card = await get_card_or_none(self.session, card_id=self.values[0])
if human_sp_card is None:
log_exception(CardNotFoundException, f'Card ID {self.values[0]} not found')
# Create a new session for this callback
from in_game.gameplay_models import engine
with Session(engine) as session:
# Get fresh game and team objects
this_game = session.get(Game, self.game_id)
this_team = session.get(Team, self.team_id)
if human_sp_card.team_id != self.team.id:
logger.error(f'Card_id {self.values[0]} does not belong to {self.team.abbrev} in Game {self.game.id}')
await interaction.channel.send(
f'Uh oh. Card ID {self.values[0]} is {human_sp_card.player.name} and belongs to {human_sp_card.team.sname}. Will you double check that before we get started?'
)
return
# Get Human SP card
human_sp_card = await get_card_or_none(session, card_id=self.values[0])
if human_sp_card is None:
log_exception(CardNotFoundException, f'Card ID {self.values[0]} not found')
await get_position(self.session, human_sp_card, 'P')
try:
legal_data = await legal_check([self.values[0]], difficulty_name=self.league_name)
if not legal_data['legal']:
await interaction.edit_original_response(
content=f'It looks like this is a Ranked Legal game and {human_sp_card.player.name_with_desc} is not legal in {self.league_name} games. You can start a new game once you pick a new SP.'
if human_sp_card.team_id != this_team.id:
logger.error(f'Card_id {self.values[0]} does not belong to {this_team.abbrev} in Game {this_game.id}')
await interaction.channel.send(
f'Uh oh. Card ID {self.values[0]} is {human_sp_card.player.name} and belongs to {human_sp_card.team.sname}. Will you double check that before we get started?'
)
return
except LegalityCheckNotRequired:
pass
return
human_sp_lineup = Lineup(
team_id=self.team.id,
player_id=human_sp_card.player.id,
card_id=self.values[0],
position='P',
batting_order=10,
is_fatigued=False,
game=self.game
)
self.session.add(human_sp_lineup)
self.session.commit()
await get_position(session, human_sp_card, 'P')
logger.info(f'trying to delete interaction: {interaction}')
try:
# await interaction.delete_original_response()
await interaction.edit_original_response(
# content=f'The {self.team.lname} are starting **{human_sp_card.player.name_with_desc}**!\n\nRun `/set lineup` to import your lineup and `/gamestate` if you are ready to play.',
content=f'The {self.team.lname} are starting **{human_sp_card.player.name_with_desc}**!',
view=None
try:
legal_data = await legal_check([self.values[0]], difficulty_name=self.league_name)
if not legal_data['legal']:
await interaction.edit_original_response(
content=f'It looks like this is a Ranked Legal game and {human_sp_card.player.name_with_desc} is not legal in {self.league_name} games. You can start a new game once you pick a new SP.'
)
return
except (LegalityCheckNotRequired, Exception) as e:
# Skip legality check if not required or if it fails
logger.info(f'Skipping legality check: {e}')
pass
human_sp_lineup = Lineup(
team_id=this_team.id,
player_id=human_sp_card.player.id,
card_id=self.values[0],
position='P',
batting_order=10,
is_fatigued=False,
game=this_game
)
except Exception as e:
log_exception(e, 'Couldn\'t clean up after selecting sp')
session.add(human_sp_lineup)
session.commit()
logger.info(f'trying to delete interaction: {interaction}')
try:
# await interaction.delete_original_response()
await interaction.edit_original_response(
# content=f'The {this_team.lname} are starting **{human_sp_card.player.name_with_desc}**!\n\nRun `/set lineup` to import your lineup and `/gamestate` if you are ready to play.',
content=f'The {this_team.lname} are starting **{human_sp_card.player.name_with_desc}**!',
view=None
)
except Exception as e:
log_exception(e, 'Couldn\'t clean up after selecting sp')
class SelectReliefPitcher(discord.ui.Select):