New show-card dropdown view

Added PlayInitException
Added complete_and_post_play for log commands
Added many more log plays
Add undo-play
Added query logging
This commit is contained in:
Cal Corum 2024-11-09 00:48:13 -06:00
parent fc3b407f2d
commit c3418c4dfd
12 changed files with 596 additions and 80 deletions

View File

@ -8,14 +8,14 @@ from discord.ext import commands, tasks
import pygsheets import pygsheets
from api_calls import db_get from api_calls import db_get
from command_logic.logic_gameplay import doubles, flyballs, get_lineups_from_sheets, checks_log_interaction, complete_play, singles from command_logic.logic_gameplay import advance_runners, bunts, doubles, flyballs, get_lineups_from_sheets, checks_log_interaction, complete_play, hit_by_pitch, homeruns, popouts, show_defense_cards, singles, strikeouts, triples, undo_play, walks
from exceptions import GameNotFoundException, TeamNotFoundException, PlayNotFoundException, GameException from exceptions import GameNotFoundException, TeamNotFoundException, PlayNotFoundException, GameException
from helpers import PD_PLAYERS_ROLE_NAME, team_role, user_has_role, random_gif, random_from_list from helpers import DEFENSE_LITERAL, PD_PLAYERS_ROLE_NAME, team_role, user_has_role, random_gif, random_from_list
# from in_game import ai_manager # from in_game import ai_manager
from in_game.ai_manager import get_starting_pitcher, get_starting_lineup 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.game_helpers import PUBLIC_FIELDS_CATEGORY_NAME, legal_check
from in_game.gameplay_models import Lineup, Session, engine, player_description, select, Game from in_game.gameplay_models import Lineup, Play, Session, engine, player_description, select, Game
from in_game.gameplay_queries import get_channel_game_or_none, get_active_games_by_team, get_game_lineups, get_team_or_none, get_card_or_none from in_game.gameplay_queries import get_channel_game_or_none, get_active_games_by_team, get_game_lineups, get_team_or_none, get_card_or_none
from utilities.buttons import Confirm from utilities.buttons import Confirm
@ -49,6 +49,25 @@ class Gameplay(commands.Cog):
logging.error(msg=error, stack_info=True) logging.error(msg=error, stack_info=True)
await ctx.send(f'{error[:1600]}') await ctx.send(f'{error[:1600]}')
async def post_play(self, session: Session, interaction: discord.Interaction, this_play: Play, buffer_message: str = None):
if buffer_message is not None:
await interaction.edit_original_response(
content=buffer_message
)
await interaction.channel.send(
content=None,
embed=this_play.game.get_scorebug_embed(session, full_length=False, classic=CLASSIC_EMBED)
)
else:
await interaction.edit_original_response(
content=None,
embed=this_play.game.get_scorebug_embed(session, full_length=False, classic=CLASSIC_EMBED)
)
async def complete_and_post_play(self, session: Session, interaction: discord.Interaction, this_play: Play, buffer_message: str = None):
complete_play(session, this_play)
await self.post_play(session, interaction, this_play, buffer_message)
group_new_game = app_commands.Group(name='new-game', description='Start a new baseball game') 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') @group_new_game.command(name='mlb-campaign', description='Start a new MLB campaign game against an AI')
@ -106,7 +125,7 @@ class Gameplay(commands.Cog):
ai_team = away_team if away_team.is_ai else home_team ai_team = away_team if away_team.is_ai else home_team
human_team = away_team if home_team.is_ai else away_team human_team = away_team if home_team.is_ai else away_team
conflict_games = get_active_games_by_team(session, team_id=human_team.id) conflict_games = get_active_games_by_team(session, team=human_team)
if len(conflict_games) > 0: if len(conflict_games) > 0:
await interaction.edit_original_response( 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}' content=f'Ope. The {human_team.sname} are already playing over in {interaction.guild.get_channel(conflict_games[0].channel_id).mention}'
@ -352,69 +371,141 @@ class Gameplay(commands.Cog):
@group_log.command(name='flyball', description='Flyballs: a, b, ballpark, bq, c') @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']): async def log_flyball(self, interaction: discord.Interaction, flyball_type: Literal['a', 'b', 'ballpark', 'b?', 'c']):
with Session(engine) as session: with Session(engine) as session:
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='flyball') this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log flyball')
this_play = await flyballs(session, interaction, this_game, this_play, flyball_type) this_play = await flyballs(session, interaction, this_play, flyball_type)
logging.info(f'log flyball {flyball_type} - this_play: {this_play}') logging.info(f'log flyball {flyball_type} - this_play: {this_play}')
complete_play(session, this_play) await self.complete_and_post_play(
session,
if this_play.starting_outs + this_play.outs < 3 and ((this_play.on_second and flyball_type == 'b') or (this_play.on_third and flyball_type == '?b')): interaction,
await interaction.edit_original_response(content='Flyball logged') this_play,
await interaction.channel.send( buffer_message='Double logged' if this_play.starting_outs + this_play.outs < 3 and ((this_play.on_second and flyball_type == 'b') or (this_play.on_third and flyball_type == '?b')) else None
content=None, )
embed=this_play.game.get_scorebug_embed(session, full_length=False, classic=CLASSIC_EMBED)
)
else:
await interaction.edit_original_response(
content=None,
embed=this_play.game.get_scorebug_embed(session, full_length=False, classic=CLASSIC_EMBED)
)
@group_log.command(name='single', description='Singles: *, **, ballpark, uncapped') @group_log.command(name='single', description='Singles: *, **, ballpark, uncapped')
async def log_single( async def log_single(
self, interaction: discord.Interaction, single_type: Literal['*', '**', 'ballpark', 'uncapped']): self, interaction: discord.Interaction, single_type: Literal['*', '**', 'ballpark', 'uncapped']):
with Session(engine) as session: with Session(engine) as session:
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='single') this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log single')
this_play = await singles(session, interaction, this_game, this_play, single_type) this_play = await singles(session, interaction, this_play, single_type)
logging.info(f'log single {single_type} - this_play: {this_play}') logging.info(f'log single {single_type} - this_play: {this_play}')
complete_play(session, this_play) await self.complete_and_post_play(session, interaction, this_play, buffer_message='Double logged' if ((this_play.on_first or this_play.on_second) and single_type == 'uncapped') else None)
if ((this_play.on_first or this_play.on_second) and single_type == 'uncapped'): # complete_play(session, this_play)
await interaction.edit_original_response(content='Single logged')
await interaction.channel.send( # if ((this_play.on_first or this_play.on_second) and single_type == 'uncapped'):
content=None, # await interaction.edit_original_response(content='Single logged')
embed=this_play.game.get_scorebug_embed(session, full_length=False, classic=CLASSIC_EMBED) # await interaction.channel.send(
) # content=None,
else: # embed=this_play.game.get_scorebug_embed(session, full_length=False, classic=CLASSIC_EMBED)
await interaction.edit_original_response( # )
content=None, # else:
embed=this_play.game.get_scorebug_embed(session, full_length=False, classic=CLASSIC_EMBED) # await interaction.edit_original_response(
) # content=None,
# embed=this_play.game.get_scorebug_embed(session, full_length=False, classic=CLASSIC_EMBED)
# )
@group_log.command(name='double', description='Doubles: **, ***, uncapped') @group_log.command(name='double', description='Doubles: **, ***, uncapped')
async def log_double(self, interaction: discord.Interaction, double_type: Literal['**', '***', 'uncapped']): async def log_double(self, interaction: discord.Interaction, double_type: Literal['**', '***', 'uncapped']):
with Session(engine) as session: with Session(engine) as session:
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='double') this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log double')
this_play = await doubles(session, interaction, this_game, this_play, double_type) this_play = await doubles(session, interaction, this_play, double_type)
logging.info(f'log double {double_type} - this_play: {this_play}') logging.info(f'log double {double_type} - this_play: {this_play}')
complete_play(session, this_play) 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')
if (this_play.on_first and double_type == 'uncapped'): this_play = await triples(session, interaction, this_play)
await interaction.edit_original_response(content='Double logged') logging.info(f'log triple - this_play: {this_play}')
await interaction.channel.send(
content=None, await self.complete_and_post_play(session, interaction, this_play)
embed=this_play.game.get_scorebug_embed(session, full_length=False, classic=CLASSIC_EMBED)
) @group_log.command(name='homerun', description='Home Runs: ballpark, no-doubt')
else: async def log_homerun(self, interaction: discord.Interaction, homerun_type: Literal['ballpark', 'no-doubt']):
await interaction.edit_original_response( with Session(engine) as session:
content=None, this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log homerun')
embed=this_play.game.get_scorebug_embed(session, full_length=False, classic=CLASSIC_EMBED)
) this_play = await homeruns(session, interaction, this_play, homerun_type)
logging.info(f'log homerun {homerun_type} - this_play: {this_play}')
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')
this_play = await walks(session, interaction, this_play, walk_type)
logging.info(f'log walk {walk_type} - this_play: {this_play}')
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')
this_play = await strikeouts(session, interaction, this_play)
logging.info(f'log strikeout - this_play: {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')
this_play = await popouts(session, interaction, this_play)
logging.info(f'log popout - this_play: {this_play}')
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')
this_play = await hit_by_pitch(session, interaction, this_play)
logging.info(f'log hit-by-pitch - this_play: {this_play}')
await self.complete_and_post_play(session, interaction, this_play)
@group_log.command(name='bunt', description='Hit by pitch: batter to first; runners advance if forced')
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')
this_play = await bunts(session, interaction, this_play, bunt_type)
logging.info(f'log bunt - this_play: {this_play}')
await self.complete_and_post_play(session, interaction, this_play)
@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')
this_play = undo_play(session, this_play)
logging.info(f'log undo-play - this_play: {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')
await show_defense_cards(session, interaction, this_play, position)
logging.info(f'show-card defense - position: {position}')
async def setup(bot): async def setup(bot):

View File

@ -4,13 +4,16 @@ import logging
import discord import discord
import pandas as pd import pandas as pd
from sqlmodel import Session, select, func from sqlmodel import Session, select, func
from sqlalchemy import delete
from typing import Literal from typing import Literal
from exceptions import * from exceptions import *
from helpers import DEFENSE_LITERAL
from in_game.game_helpers import legal_check from in_game.game_helpers import legal_check
from in_game.gameplay_models import Game, Lineup, Team, Play from in_game.gameplay_models import Game, Lineup, Team, Play
from in_game.gameplay_queries import get_card_or_none, get_channel_game_or_none, get_last_team_play, get_one_lineup, get_team_or_none, get_players_last_pa from in_game.gameplay_queries import get_card_or_none, get_channel_game_or_none, get_last_team_play, get_one_lineup, get_sorted_lineups, get_team_or_none, get_players_last_pa
from utilities.buttons import ButtonOptions, Confirm, ask_confirm from utilities.buttons import ButtonOptions, Confirm, ask_confirm
from utilities.dropdown import DropdownOptions, DropdownView, SelectViewDefense
from utilities.embeds import image_embed from utilities.embeds import image_embed
from utilities.pages import Pagination from utilities.pages import Pagination
@ -307,7 +310,7 @@ async def checks_log_interaction(session: Session, interaction: discord.Interact
this_play = this_game.current_play_or_none(session) this_play = this_game.current_play_or_none(session)
if this_play is None: if this_play is None:
logging.error(f'{command_name} command: No play found for Game ID {this_game.id} - attempting to initialize play') logging.error(f'{command_name} command: No play found for Game ID {this_game.id} - attempting to initialize play')
this_play = this_game.initialize_play(session) this_play = activate_last_play(session, this_game)
this_play.locked = True this_play.locked = True
session.add(this_play) session.add(this_play)
@ -421,11 +424,6 @@ def advance_runners(session: Session, this_play: Play, num_bases: int, is_error:
this_play.on_first_final = 2 this_play.on_first_final = 2
else: else:
this_play.on_first_final = 1 this_play.on_first_final = 1
if num_bases == 4:
this_play.batter_final = 4
this_play.rbi += 1
this_play.run = 1
return this_play return this_play
@ -511,10 +509,11 @@ async def show_outfield_cards(session: Session, interaction: discord.Interaction
return [lf, cf, rf][page_num] return [lf, cf, rf][page_num]
async def flyballs(session: Session, interaction: discord.Interaction, this_game: Game, this_play: Play, flyball_type: Literal['a', 'ballpark', 'b', 'b?', 'c']) -> Play: async def flyballs(session: Session, interaction: discord.Interaction, this_play: Play, flyball_type: Literal['a', 'ballpark', 'b', 'b?', 'c']) -> Play:
""" """
Commits this_play Commits this_play
""" """
this_game = this_play.game
num_outs = 1 num_outs = 1
if flyball_type == 'a': if flyball_type == 'a':
@ -639,7 +638,8 @@ async def flyballs(session: Session, interaction: discord.Interaction, this_game
return this_play return this_play
async def check_uncapped_advance(session: Session, interaction: discord.Interaction, this_game: Game, this_play: Play, lead_runner: Lineup, lead_base: int, trail_runner: Lineup, trail_base: int): async def check_uncapped_advance(session: Session, interaction: discord.Interaction, this_play: Play, lead_runner: Lineup, lead_base: int, trail_runner: Lineup, trail_base: int):
this_game = this_play.game
outfielder = await show_outfield_cards(session, interaction, this_play) outfielder = await show_outfield_cards(session, interaction, this_play)
logging.info(f'throw from {outfielder.player.name_with_desc}') logging.info(f'throw from {outfielder.player.name_with_desc}')
def_team = this_play.pitcher.team def_team = this_play.pitcher.team
@ -919,11 +919,11 @@ async def check_uncapped_advance(session: Session, interaction: discord.Interact
return this_play return this_play
async def singles(session: Session, interaction: discord.Interaction, this_game: Game, this_play: Play, single_type: Literal['*', '**', 'ballpark', 'uncapped']) -> Play: async def singles(session: Session, interaction: discord.Interaction, this_play: Play, single_type: Literal['*', '**', 'ballpark', 'uncapped']) -> Play:
""" """
Commits this_play Commits this_play
""" """
this_play.pa, this_play.ab, this_play.hit, this_play.batter_final = 1, 1, 1, 1 this_play.hit, this_play.batter_final = 1, 1
if single_type == '**': if single_type == '**':
advance_runners(session, this_play, num_bases=2) advance_runners(session, this_play, num_bases=2)
@ -954,7 +954,7 @@ async def singles(session: Session, interaction: discord.Interaction, this_game:
trail_runner = this_play.batter trail_runner = this_play.batter
trail_base = 2 trail_base = 2
this_play = await check_uncapped_advance(session, interaction, this_game, this_play, lead_runner, lead_base, trail_runner, trail_base) this_play = await check_uncapped_advance(session, interaction, this_play, lead_runner, lead_base, trail_runner, trail_base)
session.add(this_play) session.add(this_play)
session.commit() session.commit()
@ -963,7 +963,7 @@ async def singles(session: Session, interaction: discord.Interaction, this_game:
return this_play return this_play
async def doubles(session: Session, interaction: discord.Interaction, this_game: Game, this_play: Play, double_type: Literal['**', '***', 'uncapped']) -> Play: async def doubles(session: Session, interaction: discord.Interaction, this_play: Play, double_type: Literal['**', '***', 'uncapped']) -> Play:
""" """
Commits this_play Commits this_play
""" """
@ -979,11 +979,174 @@ async def doubles(session: Session, interaction: discord.Interaction, this_game:
this_play = advance_runners(session, this_play, num_bases=2) this_play = advance_runners(session, this_play, num_bases=2)
if this_play.on_first: if this_play.on_first:
this_play = await check_uncapped_advance(session, interaction, this_game, this_play, lead_runner=this_play.on_first, lead_base=4, trail_runner=this_play.batter, trail_base=3) this_play = await check_uncapped_advance(session, interaction, this_play, lead_runner=this_play.on_first, lead_base=4, trail_runner=this_play.batter, trail_base=3)
session.add(this_play) session.add(this_play)
session.commit() session.commit()
session.refresh(this_play) session.refresh(this_play)
return this_play return this_play
async def triples(session: Session, interaction: discord.Interaction, this_play: Play):
"""
Commits this play
"""
this_play.hit, this_play.triple, this_play.batter_final = 1, 1, 3
this_play = advance_runners(session, this_play, num_bases=3)
session.add(this_play)
session.commit()
session.refresh(this_play)
return this_play
async def homeruns(session: Session, interaction: discord.Interaction, this_play: Play, homerun_type: Literal['ballpark', 'no-doubt']):
this_play.hit, this_play.homerun, this_play.batter_final, this_play.rbi, this_play.run = 1, 1, 4, 1, 1
this_play.bphr = 1 if homerun_type == 'ballpark' else 0
this_play = advance_runners(session, this_play, num_bases=4)
session.add(this_play)
session.commit()
session.refresh(this_play)
return this_play
async def walks(session: Session, interaction: discord.Interaction, this_play: Play, walk_type: Literal['unintentional', 'intentional'] = 'unintentional'):
this_play.ab, this_play.bb, this_play.batter_final = 0, 1, 1
this_play.ibb = 1 if walk_type == 'intentional' else 0
this_play = advance_runners(session, this_play, num_bases=1, only_forced=True)
session.add(this_play)
session.commit()
session.refresh(this_play)
return this_play
async def strikeouts(session: Session, interaction: discord.Interaction, this_play: Play):
this_play.so, this_play.outs = 1, 1
this_play = advance_runners(session, this_play, num_bases=0)
session.add(this_play)
session.commit()
session.refresh(this_play)
return this_play
async def popouts(session: Session, interaction: discord.Interaction, this_play: Play):
this_play.outs = 1
this_play = advance_runners(session, this_play, num_bases=0)
session.add(this_play)
session.commit()
session.refresh(this_play)
return this_play
async def hit_by_pitch(session: Session, interaction: discord.Interaction, this_play: Play):
this_play.ab, this_play.hbp = 0, 1
this_play = advance_runners(session, this_play, num_bases=1, only_forced=True)
session.add(this_play)
session.commit()
session.refresh(this_play)
return this_play
async def bunts(session: Session, interaction: discord.Interaction, this_play: Play, bunt_type: Literal['sacrifice', 'bad', 'popout', 'double-play', 'defense']):
this_play.ab = 1 if bunt_type != 'sacrifice' else 0
this_play.sac = 1 if bunt_type != 'sacrifice' else 0
if bunt_type == 'sacrifice':
this_play = advance_runners(session, this_play, num_bases=1)
elif bunt_type == 'popout':
this_play = advance_runners(session, this_play, num_bases=0)
else:
log_exception(KeyError, f'Bunt type {bunt_type} is not yet implemented')
session.add(this_play)
session.commit()
session.refresh(this_play)
return this_play
def activate_last_play(session: Session, this_game: Game) -> Play:
p_query = session.exec(select(Play).where(Play.game == this_game).order_by(Play.id.desc()).limit(1)).all()
this_play = complete_play(session, p_query[0])
return this_play
def undo_play(session: Session, this_play: Play):
this_game = this_play.game
last_two_plays = session.exec(select(Play).where(Play.game == this_game).order_by(Play.id.desc()).limit(2)).all()
for play in last_two_plays:
for runner, to_base in [(play.on_first, play.on_first_final), (play.on_second, play.on_second_final), (play.on_third, play.on_third_final)]:
if to_base == 4:
last_pa = get_players_last_pa(session, runner)
last_pa.run, last_pa.e_run = 0, 0
session.add(last_pa)
last_two_ids = [last_two_plays[0].id, last_two_plays[1].id]
logging.warning(f'Deleting plays: {last_two_ids}')
session.exec(delete(Play).where(Play.id.in_(last_two_ids)))
session.commit()
try:
this_play = this_game.initialize_play(session)
logging.info(f'Initialized play: {this_play.id}')
except PlayInitException:
this_play = activate_last_play(session, this_game)
logging.info(f'Re-activated play: {this_play.id}')
return this_play
async def show_defense_cards(session: Session, interaction: discord.Interaction, this_play: Play, first_position: DEFENSE_LITERAL):
position_map = {
'Pitcher': 'P',
'Catcher': 'C',
'First Base': '1B',
'Second Base': '2B',
'Third Base': '3B',
'Shortstop': 'SS',
'Left Field': 'LF',
'Center Field': 'CF',
'Right Field': 'RF'
}
this_position = position_map[first_position]
sorted_lineups = get_sorted_lineups(session, this_play.game, this_play.pitcher.team)
select_player_options = [
discord.SelectOption(label=f'{x.position} - {x.player.name}', value=f'{x.id}', default=this_position == x.position) for x in sorted_lineups
]
this_lineup = get_one_lineup(session, this_play.game, this_play.pitcher.team, position=this_position)
player_embed = image_embed(
image_url=this_lineup.player.image,
color=this_play.pitcher.team.color,
author_name=this_play.pitcher.team.lname,
author_icon=this_play.pitcher.team.logo
)
player_dropdown = SelectViewDefense(
options=select_player_options,
this_play=this_play,
base_embed=player_embed,
session=session,
sorted_lineups=sorted_lineups
)
dropdown_view = DropdownView(dropdown_objects=[player_dropdown], timeout=60)
await interaction.edit_original_response(content=None, embed=player_embed, view=dropdown_view)

View File

@ -34,3 +34,7 @@ class TeamNotFoundException(GameException):
class PlayNotFoundException(GameException): class PlayNotFoundException(GameException):
pass pass
class PlayInitException(GameException):
pass

View File

@ -244,6 +244,7 @@ SELECT_CARDSET_OPTIONS = [
discord.SelectOption(label='2012 Season', value='7') discord.SelectOption(label='2012 Season', value='7')
] ]
ACTIVE_EVENT_LITERAL = Literal['1998 Season'] ACTIVE_EVENT_LITERAL = Literal['1998 Season']
DEFENSE_LITERAL = Literal['Pitcher', 'Catcher', 'First Base', 'Second Base', 'Third Base', 'Shortstop', 'Left Field', 'Center Field', 'Right Field']
class Question: class Question:

View File

@ -128,7 +128,7 @@ class Game(SQLModel, table=True):
return f'{pri_cardsets}{back_cardsets}' return f'{pri_cardsets}{back_cardsets}'
def current_play_or_none(self, session: Session): def current_play_or_none(self, session: Session):
this_play = session.exec(select(Play).where(Play.game == self).order_by(Play.id.desc()).limit(1)).all() this_play = session.exec(select(Play).where(Play.game == self, Play.complete == False).order_by(Play.id.desc()).limit(1)).all()
if len(this_play) == 1: if len(this_play) == 1:
return this_play[0] return this_play[0]
else: else:
@ -231,6 +231,10 @@ class Game(SQLModel, table=True):
if existing_play is not None: if existing_play is not None:
return existing_play return existing_play
all_plays = session.exec(select(func.count(Play.id)).where(Play.game == self)).one()
if all_plays > 0:
raise PlayInitException(f'{all_plays} plays for game {self.id} already exist, but all are complete.')
leadoff_batter, home_pitcher, home_catcher = None, None, None leadoff_batter, home_pitcher, home_catcher = None, None, None
home_positions, away_positions = [], [] home_positions, away_positions = [], []
for line in [x for x in self.lineups if x.active]: for line in [x for x in self.lineups if x.active]:
@ -796,9 +800,7 @@ class Play(PlayBase, table=True):
# Defensive Alignment # Defensive Alignment
if self.on_third and self.starting_outs < 2: if self.on_third and self.starting_outs < 2:
if self.on_first: if abs(self.away_score - self.home_score) <= 3:
ai_note += f'- play the corners in\n'
elif abs(self.away_score - self.home_score) <= 3:
ai_note += f'- play the whole infield in\n' ai_note += f'- play the whole infield in\n'
else: else:
ai_note += f'- play the corners in\n' ai_note += f'- play the corners in\n'

View File

@ -8,10 +8,12 @@ from exceptions import log_exception, PlayNotFoundException
def get_games_by_channel(session: Session, channel_id: int) -> list[Game]: def get_games_by_channel(session: Session, channel_id: int) -> list[Game]:
logging.info(f'Getting games in channel {channel_id}')
return session.exec(select(Game).where(Game.channel_id == channel_id, Game.active)).all() return session.exec(select(Game).where(Game.channel_id == channel_id, Game.active)).all()
def get_channel_game_or_none(session: Session, channel_id: int) -> Game | None: def get_channel_game_or_none(session: Session, channel_id: int) -> Game | None:
logging.info(f'Getting one game from channel {channel_id}')
all_games = get_games_by_channel(session, channel_id) all_games = get_games_by_channel(session, channel_id)
if len(all_games) > 1: if len(all_games) > 1:
err = 'Too many games found in get_channel_game_or_none' err = 'Too many games found in get_channel_game_or_none'
@ -22,12 +24,14 @@ def get_channel_game_or_none(session: Session, channel_id: int) -> Game | None:
return all_games[0] return all_games[0]
def get_active_games_by_team(session: Session, team_id: int) -> list[Game]: def get_active_games_by_team(session: Session, team: Team) -> list[Game]:
return session.exec(select(Game).where(Game.active, or_(Game.away_team_id == team_id, Game.home_team_id == team_id))).all() logging.info(f'Getting game for team {team.lname}')
return session.exec(select(Game).where(Game.active, or_(Game.away_team_id == team.id, Game.home_team_id == team.id))).all()
async def get_team_or_none( async def get_team_or_none(
session: Session, team_id: int | None = None, gm_id: int | None = None, team_abbrev: str | None = None, skip_cache: bool = False) -> Team | None: session: Session, team_id: int | None = None, gm_id: int | None = None, team_abbrev: str | None = None, skip_cache: bool = False) -> Team | None:
logging.info(f'Getting team or none / team_id: {team_id} / gm_id: {gm_id} / team_abbrev: {team_abbrev} / skip_cache: {skip_cache}')
if team_id is None and gm_id is None and team_abbrev is None: if team_id is None and gm_id is None and team_abbrev is None:
err = 'One of "team_id", "gm_id", or "team_abbrev" must be included in search' err = 'One of "team_id", "gm_id", or "team_abbrev" must be included in search'
logging.error(f'gameplay_models - get_team - {err}') logging.error(f'gameplay_models - get_team - {err}')
@ -120,6 +124,7 @@ async def get_player_or_none(session: Session, player_id: int, skip_cache: bool
def get_player_id_from_dict(json_data: dict) -> int: def get_player_id_from_dict(json_data: dict) -> int:
logging.info(f'Getting player from dict {json_data}')
if 'player_id' in json_data: if 'player_id' in json_data:
return json_data['player_id'] return json_data['player_id']
elif 'id' in json_data: elif 'id' in json_data:
@ -130,6 +135,7 @@ def get_player_id_from_dict(json_data: dict) -> int:
async def get_or_create_ai_card(session: Session, player: Player, team: Team, skip_cache: bool = False, dev_mode: bool = False) -> Card: async def get_or_create_ai_card(session: Session, player: Player, team: Team, skip_cache: bool = False, dev_mode: bool = False) -> Card:
logging.info(f'Getting or creating card for {player.name_with_desc} on the {team.sname}')
if not team.is_ai: if not team.is_ai:
err = f'Cannot create AI cards for human teams' err = f'Cannot create AI cards for human teams'
logging.error(f'gameplay_models - get_or_create_ai_card: {err}') logging.error(f'gameplay_models - get_or_create_ai_card: {err}')
@ -196,6 +202,7 @@ async def get_or_create_ai_card(session: Session, player: Player, team: Team, sk
async def get_card_or_none(session: Session, card_id: int, skip_cache: bool = False) -> Card | None: async def get_card_or_none(session: Session, card_id: int, skip_cache: bool = False) -> Card | None:
logging.info(f'Getting card {card_id}')
if not skip_cache: if not skip_cache:
this_card = session.get(Card, card_id) this_card = session.get(Card, card_id)
@ -236,6 +243,7 @@ async def get_card_or_none(session: Session, card_id: int, skip_cache: bool = Fa
def get_game_lineups(session: Session, this_game: Game, specific_team: Team = None, is_active: bool = None) -> list[Lineup]: def get_game_lineups(session: Session, this_game: Game, specific_team: Team = None, is_active: bool = None) -> list[Lineup]:
logging.info(f'Getting lineups for game {this_game.id} / specific_team: {specific_team} / is_active: {is_active}')
st = select(Lineup).where(Lineup.game == this_game) st = select(Lineup).where(Lineup.game == this_game)
if specific_team is not None: if specific_team is not None:
@ -247,6 +255,7 @@ def get_game_lineups(session: Session, this_game: Game, specific_team: Team = No
def get_players_last_pa(session: Session, lineup_member: Lineup, none_okay: bool = False): def get_players_last_pa(session: Session, lineup_member: Lineup, none_okay: bool = False):
logging.info(f'Getting last AB for {lineup_member.player.name_with_desc} on the {lineup_member.team.lname}')
last_pa = session.exec(select(Play).where(Play.game == lineup_member.game, Play.batter == lineup_member).order_by(Play.id.desc()).limit(1)).all() last_pa = session.exec(select(Play).where(Play.game == lineup_member.game, Play.batter == lineup_member).order_by(Play.id.desc()).limit(1)).all()
if len(last_pa) == 1: if len(last_pa) == 1:
return last_pa[0] return last_pa[0]
@ -258,6 +267,7 @@ def get_players_last_pa(session: Session, lineup_member: Lineup, none_okay: bool
def get_one_lineup(session: Session, this_game: Game, this_team: Team, active: bool = True, position: str = None, batting_order: int = None) -> Lineup: def get_one_lineup(session: Session, this_game: Game, this_team: Team, active: bool = True, position: str = None, batting_order: int = None) -> Lineup:
logging.info(f'Getting one lineup / this_game: {this_game.id} / this_team: {this_team.lname} / active: {active}, position: {position}, batting_order: {batting_order}')
if position is None and batting_order is None: if position is None and batting_order is None:
raise KeyError('Position or batting order must be provided for get_one_lineup') raise KeyError('Position or batting order must be provided for get_one_lineup')
@ -271,6 +281,7 @@ def get_one_lineup(session: Session, this_game: Game, this_team: Team, active: b
def get_last_team_play(session: Session, this_game: Game, this_team: Team, none_okay: bool = False): def get_last_team_play(session: Session, this_game: Game, this_team: Team, none_okay: bool = False):
logging.info(f'Getting last play for the {this_team.lname} in game {this_game.id}')
last_play = session.exec(select(Play).join(Lineup, onclause=Lineup.id == Play.batter_id).where(Play.game == this_game, Lineup.team == this_team).order_by(Play.id.desc()).limit(1)).all() last_play = session.exec(select(Play).join(Lineup, onclause=Lineup.id == Play.batter_id).where(Play.game == this_game, Lineup.team == this_team).order_by(Play.id.desc()).limit(1)).all()
if len(last_play) == 1: if len(last_play) == 1:
@ -279,4 +290,15 @@ def get_last_team_play(session: Session, this_game: Game, this_team: Team, none_
if none_okay: if none_okay:
return None return None
else: else:
log_exception(PlayNotFoundException, f'No last play found for the {this_team.sname}') log_exception(PlayNotFoundException, f'No last play found for the {this_team.sname}')
def get_sorted_lineups(session: Session, this_game: Game, this_team: Team) -> list[Lineup]:
logging.info(f'Getting sorted lineups for the {this_team.lname} in game {this_game.id}')
custom_order = {'P': 1, 'C': 2, '1B': 3, '2B': 4, '3B': 5, 'SS': 6, 'LF': 7, 'CF': 8, 'RF': 9}
all_lineups = session.exec(select(Lineup).where(Lineup.game == this_game, Lineup.active == True, Lineup.team == this_team)).all()
sorted_lineups = sorted(all_lineups, key=lambda x: custom_order.get(x.position, float('inf')))
return sorted_lineups

View File

@ -1,7 +1,7 @@
import pytest import pytest
from sqlmodel import Session, select, func from sqlmodel import Session, select, func
from command_logic.logic_gameplay import advance_runners, get_obc, get_re24, get_wpa, complete_play, log_run_scored from command_logic.logic_gameplay import advance_runners, doubles, get_obc, get_re24, get_wpa, complete_play, log_run_scored, strikeouts
from in_game.gameplay_models import Lineup, Play from in_game.gameplay_models import Lineup, Play
from tests.factory import session_fixture, Game from tests.factory import session_fixture, Game
@ -35,6 +35,7 @@ def test_advance_runners(session: Session):
assert play_3.on_second is None assert play_3.on_second is None
assert play_3.on_first is not None assert play_3.on_first is not None
def test_get_obc(): def test_get_obc():
assert get_obc() == 0 assert get_obc() == 0
assert get_obc(on_first=True) == 1 assert get_obc(on_first=True) == 1
@ -161,5 +162,35 @@ def test_log_run_scored(session: Session):
assert e_runs == 1 assert e_runs == 1
async def test_strikeouts(session: Session):
game_1 = session.get(Game, 1)
play_1 = session.get(Play, 1)
play_1 = await strikeouts(session, None, game_1, play_1)
assert play_1.so == 1
assert play_1.outs == 1
assert play_1.batter_final is None
async def test_doubles(session: Session):
game_1 = session.get(Game, 1)
play_1 = session.get(Play, 1)
play_1.hit, play_1.batter_final = 1, 1
play_2 = complete_play(session, play_1)
assert play_2.play_num == 2
play_2_ghost_1 = await doubles(session, None, game_1, play_2, double_type='***')
assert play_2_ghost_1.double == 1
assert play_2_ghost_1.on_first_final == 4
assert play_2_ghost_1.rbi == 1
play_2_ghost_2 = await doubles(session, None, game_1, play_2, double_type='**')
assert play_2_ghost_2.double == 1
assert play_2_ghost_2.rbi == 0
assert play_2_ghost_2.on_first_final == 3

View File

@ -159,7 +159,8 @@ def session_fixture():
catcher=all_lineups[10], catcher=all_lineups[10],
pa=1, pa=1,
so=1, so=1,
outs=1 outs=1,
complete=True
) )
game_1_play_2 = Play( game_1_play_2 = Play(
game=game_1, game=game_1,

View File

@ -3,6 +3,7 @@ from sqlalchemy import delete as sadelete
from sqlalchemy.sql.functions import sum, count from sqlalchemy.sql.functions import sum, count
from sqlmodel import Session, delete from sqlmodel import Session, delete
from command_logic.logic_gameplay import complete_play, singles, undo_play
from in_game.gameplay_models import Game, Lineup, GameCardsetLink, Play, select from in_game.gameplay_models import Game, Lineup, GameCardsetLink, Play, select
from in_game.gameplay_queries import get_channel_game_or_none, get_active_games_by_team from in_game.gameplay_queries import get_channel_game_or_none, get_active_games_by_team
from tests.factory import session_fixture from tests.factory import session_fixture
@ -138,6 +139,17 @@ def test_sum_function(session: Session):
assert False == False assert False == False
def test_current_play_or_none(session: Session):
game_1 = session.get(Game, 1)
this_play = game_1.initialize_play(session)
assert this_play.play_num == 2
this_play.complete = True
session.add(this_play)
session.commit()
def test_initialize_play(session: Session): def test_initialize_play(session: Session):
game_1 = session.get(Game, 1) game_1 = session.get(Game, 1)
game_3 = session.get(Game, 3) game_3 = session.get(Game, 3)
@ -155,4 +167,36 @@ def test_initialize_play(session: Session):
assert g3_play.play_num == 1 assert g3_play.play_num == 1
assert g3_play.starting_outs == 0 assert g3_play.starting_outs == 0
assert len(play_count) == 3 assert len(play_count) == 3
async def test_undo_play(session: Session):
game_1 = session.get(Game, 1)
this_play = game_1.initialize_play(session)
assert this_play.play_num == 2
all_play_ids = session.exec(select(Play.id, Play.play_num).where(Play.game == game_1)).all()
assert len(all_play_ids) == 2
assert all_play_ids[0][0] == 1
assert all_play_ids[1][1] == 2
await singles(session, None, this_play, '*')
play_3 = complete_play(session, this_play)
await singles(session, None, play_3, '*')
play_4 = complete_play(session, play_3)
on_second_play_4 = play_4.on_second
await singles(session, None, play_4, '*')
play_5 = complete_play(session, play_4)
assert len(play_5.game.plays) == 5
assert play_5.on_base_code == 7
new_play = undo_play(session, this_play)
assert len(new_play.game.plays) == 4
assert new_play.play_num == 4
assert new_play.on_second == on_second_play_4

View File

@ -2,7 +2,7 @@ import pytest
from sqlmodel import Session, select from sqlmodel import Session, select
from in_game.gameplay_models import Game, Lineup from in_game.gameplay_models import Game, Lineup
from in_game.gameplay_queries import get_game_lineups, get_one_lineup from in_game.gameplay_queries import get_game_lineups, get_one_lineup, get_sorted_lineups
from tests.factory import session_fixture from tests.factory import session_fixture
@ -60,13 +60,20 @@ def test_get_one_lineup(session: Session):
assert str(exc_info) == "<ExceptionInfo KeyError('Position or batting order must be provided for get_one_lineup') tblen=2>" assert str(exc_info) == "<ExceptionInfo KeyError('Position or batting order must be provided for get_one_lineup') tblen=2>"
# def test_lineup_substitution(session: Session, new_games_with_lineups: list[Game]): def test_order_lineups_by_position(session: Session):
# game_1 = new_games_with_lineups[0] this_game = session.get(Game, 1)
# game_2 = new_games_with_lineups[1] all_lineups = get_sorted_lineups(session, this_game, this_game.home_team)
assert all_lineups[0].position == 'P'
assert all_lineups[1].position == 'C'
assert all_lineups[2].position == '1B'
assert all_lineups[3].position == '2B'
assert all_lineups[4].position == '3B'
assert all_lineups[5].position == 'SS'
assert all_lineups[6].position == 'LF'
assert all_lineups[7].position == 'CF'
assert all_lineups[8].position == 'RF'
# session.add(game_1)
# session.add(game_2)
# session.commit()

View File

@ -1,7 +1,7 @@
import pytest import pytest
from sqlmodel import Session, select, func from sqlmodel import Session, select, func
from command_logic.logic_gameplay import complete_play, singles from command_logic.logic_gameplay import complete_play, singles, undo_play
from db_calls_gameplay import advance_runners from db_calls_gameplay import advance_runners
from in_game.gameplay_models import Lineup, Play, Game from in_game.gameplay_models import Lineup, Play, Game
from in_game.gameplay_queries import get_last_team_play from in_game.gameplay_queries import get_last_team_play
@ -77,3 +77,33 @@ def test_query_scalars(session: Session):
assert outs == 1 assert outs == 1
async def test_undo_play(session: Session):
game_1 = session.get(Game, 1)
play_2 = game_1.initialize_play(session)
assert play_2.play_num == 2
play_2 = await singles(session, None, play_2, single_type='*')
play_3 = complete_play(session, play_2)
assert play_3.play_num == 3
assert play_3.on_first == play_2.batter
play_3 = await singles(session, None, play_3, single_type='**')
play_4 = complete_play(session, play_3)
all_plays = session.exec(select(Play).where(Play.game == game_1)).all()
assert play_4.play_num == 4
assert play_4.on_first == play_3.batter
assert len(all_plays) == 4
undone_play = undo_play(session, play_4)
assert undone_play.play_num == 3
all_plays = session.exec(select(Play).where(Play.game == game_1)).all()
assert len(all_plays) == 3

View File

@ -1,7 +1,12 @@
import discord import discord
import logging import logging
class DropdownOption(discord.ui.Select): from sqlmodel import Session
from in_game.gameplay_models import Lineup, Play
from in_game.gameplay_queries import get_sorted_lineups
class DropdownOptions(discord.ui.Select):
def __init__(self, option_list: list, placeholder: str = 'Make your selection', min_values: int = 1, max_values: int = 1, callback=None): def __init__(self, option_list: list, placeholder: str = 'Make your selection', min_values: int = 1, max_values: int = 1, callback=None):
# Set the options that will be presented inside the dropdown # Set the options that will be presented inside the dropdown
# options = [ # options = [
@ -38,9 +43,124 @@ class DropdownView(discord.ui.View):
""" """
https://discordpy.readthedocs.io/en/latest/interactions/api.html#select https://discordpy.readthedocs.io/en/latest/interactions/api.html#select
""" """
def __init__(self, dropdown_objects: list[DropdownOption], timeout: float = 300.0): def __init__(self, dropdown_objects: list[discord.ui.Select], timeout: float = 300.0):
super().__init__(timeout=timeout) super().__init__(timeout=timeout)
# self.add_item(Dropdown()) # self.add_item(Dropdown())
for x in dropdown_objects: for x in dropdown_objects:
self.add_item(x) self.add_item(x)
class SelectViewDefense(discord.ui.Select):
def __init__(self, options: list, this_play: Play, base_embed: discord.Embed, session: Session, sorted_lineups: list[Lineup]):
self.embed = base_embed
self.session = session
self.play = this_play
self.sorted_lineups = sorted_lineups
super().__init__(options=options)
async def callback(self, interaction: discord.Interaction):
logging.info(f'SelectViewDefense - selection: {self.values[0]}')
this_lineup = self.session.get(Lineup, self.values[0])
self.embed.set_image(url=this_lineup.player.image)
select_player_options = [
discord.SelectOption(label=f'{x.position} - {x.player.name}', value=f'{x.id}', default=this_lineup.position == x.position) for x in self.sorted_lineups
]
player_dropdown = SelectViewDefense(
options=select_player_options,
this_play=self.play,
base_embed=self.embed,
session=self.session,
sorted_lineups=self.sorted_lineups
)
new_view = DropdownView(
dropdown_objects=[player_dropdown],
timeout=60
)
await interaction.response.edit_message(content=None, embed=self.embed, view=new_view)
class SelectOpenPack(discord.ui.Select):
def __init__(self, options: list, team: dict):
self.owner_team = team
super().__init__(placeholder='Select a Pack Type', options=options)
async def callback(self, interaction: discord.Interaction):
logging.info(f'SelectPackChoice - selection: {self.values[0]}')
pack_vals = self.values[0].split('-')
logging.info(f'pack_vals: {pack_vals}')
# Get the selected packs
params = [('team_id', self.owner_team['id']), ('opened', False), ('limit', 5), ('exact_match', True)]
open_type = 'standard'
if 'Standard' in pack_vals:
open_type = 'standard'
params.append(('pack_type_id', 1))
elif 'Premium' in pack_vals:
open_type = 'standard'
params.append(('pack_type_id', 3))
elif 'Daily' in pack_vals:
params.append(('pack_type_id', 4))
elif 'Promo Choice' in pack_vals:
open_type = 'choice'
params.append(('pack_type_id', 9))
elif 'MVP' in pack_vals:
open_type = 'choice'
params.append(('pack_type_id', 5))
elif 'All Star' in pack_vals:
open_type = 'choice'
params.append(('pack_type_id', 6))
elif 'Mario' in pack_vals:
open_type = 'choice'
params.append(('pack_type_id', 7))
elif 'Team Choice' in pack_vals:
open_type = 'choice'
params.append(('pack_type_id', 8))
else:
raise KeyError(f'Cannot identify pack details: {pack_vals}')
# If team isn't already set on team choice pack, make team pack selection now
await interaction.response.edit_message(view=None)
cardset_id = None
if 'Team Choice' in pack_vals and 'Cardset' in pack_vals:
# cardset_id = pack_vals[2]
cardset_index = pack_vals.index('Cardset')
cardset_id = pack_vals[cardset_index + 1]
params.append(('pack_cardset_id', cardset_id))
if 'Team' not in pack_vals:
view = SelectView(
[SelectChoicePackTeam('AL', self.owner_team, cardset_id),
SelectChoicePackTeam('NL', self.owner_team, cardset_id)],
timeout=30
)
await interaction.channel.send(
content=None,
view=view
)
return
params.append(('pack_team_id', pack_vals[pack_vals.index('Team') + 1]))
else:
if 'Team' in pack_vals:
params.append(('pack_team_id', pack_vals[pack_vals.index('Team') + 1]))
if 'Cardset' in pack_vals:
cardset_id = pack_vals[pack_vals.index('Cardset') + 1]
params.append(('pack_cardset_id', cardset_id))
p_query = await db_get('packs', params=params)
if p_query['count'] == 0:
logging.error(f'open-packs - no packs found with params: {params}')
raise ValueError(f'Unable to open packs')
# Open the packs
if open_type == 'standard':
await open_st_pr_packs(p_query['packs'], self.owner_team, interaction)
elif open_type == 'choice':
await open_choice_pack(p_query['packs'][0], self.owner_team, interaction, cardset_id)