Added /substitution defense

This commit is contained in:
Cal Corum 2025-05-30 01:19:45 -05:00
parent e9c9a3f392
commit 17680a2348
6 changed files with 168 additions and 23 deletions

View File

@ -14,11 +14,11 @@ import sqlalchemy
from sqlmodel import func, or_ from sqlmodel import func, or_
from api_calls import db_get from api_calls import db_get
from command_logic.logic_gameplay import bunts, chaos, complete_game, 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 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 dice import ab_roll
from exceptions import * from exceptions import *
import gauntlets import gauntlets
from helpers import CARDSETS, DEFENSE_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 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 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
@ -1215,6 +1215,22 @@ class Gameplay(commands.Cog):
view=rp_view 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 = app_commands.Group(name='log', description='Log a play in this channel\'s game')

View File

@ -14,14 +14,14 @@ from api_calls import db_delete, db_get, db_post
from dice import DTwentyRoll, d_twenty_roll, frame_plate_check, sa_fielding_roll from dice import DTwentyRoll, d_twenty_roll, frame_plate_check, sa_fielding_roll
from exceptions import * from exceptions import *
from gauntlets import post_result from gauntlets import post_result
from helpers import COLORS, DEFENSE_LITERAL, SBA_COLOR, get_channel, team_role from helpers import COLORS, DEFENSE_LITERAL, DEFENSE_NO_PITCHER_LITERAL, SBA_COLOR, get_channel, position_name_to_abbrev, team_role
from in_game.ai_manager import get_starting_lineup from in_game.ai_manager import 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 BattingCard, Card, Game, Lineup, PositionRating, RosterLink, Team, Play from in_game.gameplay_models import BattingCard, Card, Game, Lineup, PositionRating, RosterLink, Team, Play
from in_game.gameplay_queries import get_active_games_by_team, get_available_batters, get_batter_card, get_batting_statline, get_game_cardset_links, get_or_create_ai_card, get_pitcher_runs_by_innings, get_pitching_statline, get_plays_by_pitcher, get_position, get_available_pitchers, get_card_or_none, get_channel_game_or_none, get_db_ready_decisions, get_db_ready_plays, get_game_lineups, get_last_team_play, get_one_lineup, get_player_id_from_dict, get_player_name_from_dict, get_player_or_none, get_sorted_lineups, get_team_or_none, get_players_last_pa, post_game_rewards from in_game.gameplay_queries import get_active_games_by_team, get_available_batters, get_batter_card, get_batting_statline, get_game_cardset_links, get_or_create_ai_card, get_pitcher_runs_by_innings, get_pitching_statline, get_plays_by_pitcher, get_position, get_available_pitchers, get_card_or_none, get_channel_game_or_none, get_db_ready_decisions, get_db_ready_plays, get_game_lineups, get_last_team_play, get_one_lineup, get_player_id_from_dict, get_player_name_from_dict, get_player_or_none, get_sorted_lineups, get_team_or_none, get_players_last_pa, post_game_rewards
from in_game.managerai_responses import DefenseResponse from in_game.managerai_responses import DefenseResponse
from utilities.buttons import ButtonOptions, Confirm, ask_confirm, ask_with_buttons from utilities.buttons import ButtonOptions, Confirm, ask_confirm, ask_with_buttons
from utilities.dropdown import DropdownView, SelectBatterSub, SelectReliefPitcher, SelectStartingPitcher, SelectViewDefense from utilities.dropdown import DropdownView, SelectBatterSub, SelectDefensiveChange, SelectReliefPitcher, SelectStartingPitcher, SelectViewDefense
from utilities.embeds import image_embed from utilities.embeds import image_embed
from utilities.pages import Pagination from utilities.pages import Pagination
@ -361,6 +361,34 @@ def relief_pitcher_dropdown_view(session: Session, this_game: Game, human_team:
return DropdownView(dropdown_objects=[rp_selection]) return DropdownView(dropdown_objects=[rp_selection])
async def defender_dropdown_view(session: Session, this_game: Game, human_team: Team, new_position: DEFENSE_NO_PITCHER_LITERAL, responders: list[discord.User] = None):
active_players = get_game_lineups(session, this_game, human_team, is_active=True)
first_pass = [x for x in active_players if x.position != 'P' and x.batting_order != 10]
if len(first_pass) == 0:
log_exception(MissingRosterException, 'No active defenders were found to make defensive change')
defender_list = []
for x in first_pass:
this_pos = session.exec(select(PositionRating).where(PositionRating.player_id == x.player.id, PositionRating.position == position_name_to_abbrev(new_position), PositionRating.variant == x.card.variant)).all()
if len(this_pos) > 0:
defender_list.append(x)
if len(defender_list) == 0:
log_exception(PlayerNotFoundException, f'I dont see any legal defenders for {new_position} on the field.')
defender_selection = SelectDefensiveChange(
this_game=this_game,
this_team=human_team,
new_position=new_position,
session=session,
responders=responders,
placeholder=f'Who is moving to {new_position}?',
options=[SelectOption(label=f'{x.player.name_with_desc}', value=x.id) for x in defender_list]
)
return DropdownView(dropdown_objects=[defender_selection])
def sub_batter_dropdown_view(session: Session, this_game: Game, human_team: Team, batting_order: int, responders: list[discord.User]): def sub_batter_dropdown_view(session: Session, this_game: Game, human_team: Team, batting_order: int, responders: list[discord.User]):
batters = get_available_batters(session, this_game, human_team) batters = get_available_batters(session, this_game, human_team)
logger.info(f'batters: {batters}') logger.info(f'batters: {batters}')

View File

@ -17,6 +17,7 @@ from difflib import get_close_matches
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional, Literal from typing import Optional, Literal
from exceptions import log_exception
from in_game.gameplay_models import Team from in_game.gameplay_models import Team
@ -303,6 +304,7 @@ SELECT_CARDSET_OPTIONS = [
] ]
ACTIVE_EVENT_LITERAL = Literal['2025 Season'] ACTIVE_EVENT_LITERAL = Literal['2025 Season']
DEFENSE_LITERAL = Literal['Pitcher', 'Catcher', 'First Base', 'Second Base', 'Third Base', 'Shortstop', 'Left Field', 'Center Field', 'Right Field'] DEFENSE_LITERAL = Literal['Pitcher', 'Catcher', 'First Base', 'Second Base', 'Third Base', 'Shortstop', 'Left Field', 'Center Field', 'Right Field']
DEFENSE_NO_PITCHER_LITERAL = Literal['Catcher', 'First Base', 'Second Base', 'Third Base', 'Shortstop', 'Left Field', 'Center Field', 'Right Field']
COLORS = { COLORS = {
'sba': int('a6ce39', 16), 'sba': int('a6ce39', 16),
'yellow': int('FFEA00', 16), 'yellow': int('FFEA00', 16),
@ -3439,3 +3441,24 @@ def user_has_role(user: discord.User | discord.Member, role_name: str) -> bool:
def random_insult() -> str: def random_insult() -> str:
return random_from_list(INSULTS) return random_from_list(INSULTS)
def position_name_to_abbrev(position_name):
if position_name == 'Catcher':
return 'C'
elif position_name == 'First Base':
return '1B'
elif position_name == 'Second Base':
return '2B'
elif position_name == 'Third Base':
return '3B'
elif position_name == 'Shortstop':
return 'SS'
elif position_name == 'Left Field':
return 'LF'
elif position_name == 'Center Field':
return 'CF'
elif position_name == 'Right Field':
return 'RF'
else:
log_exception(NameError, f'{position_name} not recognized')

View File

@ -1046,6 +1046,7 @@ class Card(CardBase, table=True):
player: Player = Relationship(back_populates='cards') player: Player = Relationship(back_populates='cards')
team: Team = Relationship(back_populates='cards') team: Team = Relationship(back_populates='cards')
lineups: list['Lineup'] = Relationship(back_populates='card', cascade_delete=True) lineups: list['Lineup'] = Relationship(back_populates='card', cascade_delete=True)
variant: int = Field(default=0, index=True)
batterscouting: BatterScouting = Relationship(back_populates='cards') batterscouting: BatterScouting = Relationship(back_populates='cards')
pitcherscouting: PitcherScouting = Relationship(back_populates='cards') pitcherscouting: PitcherScouting = Relationship(back_populates='cards')

View File

@ -255,7 +255,7 @@ async def get_player_or_none(session: Session, player_id: int, skip_cache: bool
async def get_batter_scouting_or_none(session: Session, card: Card, skip_cache: bool = False) -> BatterScouting | None: async def get_batter_scouting_or_none(session: Session, card: Card, skip_cache: bool = False) -> BatterScouting | None:
logger.info(f'Getting batting scouting for card ID #{card.id}: {card.player.name_with_desc} / skip_cache: {skip_cache}') logger.info(f'Getting batting scouting for card ID #{card.id}: {card.player.name_with_desc} / skip_cache: {skip_cache}')
s_query = await db_get(f'battingcardratings/player/{card.player.id}', none_okay=False) s_query = await db_get(f'battingcardratings/player/{card.player.id}?variant={card.variant}', none_okay=False)
if s_query['count'] != 2: if s_query['count'] != 2:
log_exception(DatabaseError, f'Scouting for {card.player.name_with_desc} was not found.') log_exception(DatabaseError, f'Scouting for {card.player.name_with_desc} was not found.')
@ -390,7 +390,7 @@ async def get_batter_scouting_or_none(session: Session, card: Card, skip_cache:
async def get_pitcher_scouting_or_none(session: Session, card: Card, skip_cache: bool = False) -> PitcherScouting | None: async def get_pitcher_scouting_or_none(session: Session, card: Card, skip_cache: bool = False) -> PitcherScouting | None:
logger.info(f'Getting pitching scouting for card ID #{card.id}: {card.player.name_with_desc}') logger.info(f'Getting pitching scouting for card ID #{card.id}: {card.player.name_with_desc}')
s_query = await db_get(f'pitchingcardratings/player/{card.player.id}', none_okay=False) s_query = await db_get(f'pitchingcardratings/player/{card.player.id}?variant={card.variant}', none_okay=False)
if s_query['count'] != 2: if s_query['count'] != 2:
log_exception(DatabaseError, f'Scouting for {card.player.name_with_desc} was not found.') log_exception(DatabaseError, f'Scouting for {card.player.name_with_desc} was not found.')
@ -826,7 +826,13 @@ def get_one_lineup(session: Session, this_game: Game, this_team: Team, active: b
else: else:
st = st.where(Lineup.batting_order == batting_order) st = st.where(Lineup.batting_order == batting_order)
return session.exec(st).one() logger.info(f'get_one_lineup query: {st}')
compiled = st.compile(compile_kwargs={"literal_binds": True})
logger.info(f'get_one_lineup literal SQL: {compiled}')
this_lineup = session.exec(st).one()
logger.info(f'Found lineup: {this_lineup}')
return this_lineup
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):

View File

@ -9,11 +9,11 @@ from discord.utils import MISSING
from sqlmodel import Session from sqlmodel import Session
from api_calls import db_delete, db_get, db_post from api_calls import db_delete, db_get, db_post
from exceptions import CardNotFoundException, LegalityCheckNotRequired, PlayNotFoundException, PositionNotFoundException, log_exception, log_errors from exceptions import CardNotFoundException, LegalityCheckNotRequired, LineupsMissingException, PlayNotFoundException, PositionNotFoundException, log_exception, log_errors
from helpers import get_card_embeds, random_insult from helpers import DEFENSE_NO_PITCHER_LITERAL, get_card_embeds, position_name_to_abbrev, random_insult
from in_game.game_helpers import legal_check from in_game.game_helpers import legal_check
from in_game.gameplay_models import Game, Lineup, Play, Team from in_game.gameplay_models import Game, Lineup, Play, Team
from in_game.gameplay_queries import get_one_lineup, get_position, get_card_or_none from in_game.gameplay_queries import get_game_lineups, get_one_lineup, get_position, get_card_or_none
from utilities.buttons import ask_confirm, ask_position from utilities.buttons import ask_confirm, ask_position
from utilities.embeds import image_embed from utilities.embeds import image_embed
@ -215,6 +215,19 @@ class SelectReliefPitcher(discord.ui.Select):
if human_rp_card.pitcherscouting.pitchingcard.relief_rating < 2: if human_rp_card.pitcherscouting.pitchingcard.relief_rating < 2:
this_play.in_pow = True this_play.in_pow = True
logger.info(f'De-activating the old pitcher')
this_play.pitcher.active = False
self.session.add(this_play.pitcher)
logger.info(f'Checking for batting order != 10 ({self.batting_order})')
if self.batting_order != 10:
logger.info(f'Getting the player in the {self.batting_order} spot')
this_lineup = get_one_lineup(self.session, self.game, self.team, active=True, batting_order=self.batting_order)
logger.info(f'subbing lineup: {this_lineup.player.name_with_desc}')
# if this_lineup != this_play.pitcher:
this_lineup.active = False
self.session.add(this_lineup)
logger.info(f'Adding the RP lineup') logger.info(f'Adding the RP lineup')
human_rp_lineup = Lineup( human_rp_lineup = Lineup(
team_id=self.team.id, team_id=self.team.id,
@ -229,19 +242,6 @@ class SelectReliefPitcher(discord.ui.Select):
) )
self.session.add(human_rp_lineup) self.session.add(human_rp_lineup)
logger.info(f'De-activating the old pitcher')
this_play.pitcher.active = False
self.session.add(this_play.pitcher)
logger.info(f'Checking for batting order != 10 ({self.batting_order})')
if self.batting_order != 10:
logger.info(f'Getting the player in the {self.batting_order} spot')
this_lineup = get_one_lineup(self.session, self.game, self.team, active=True, batting_order=self.batting_order)
logger.info(f'subbing lineup: {this_lineup.player.name_with_desc}')
if this_lineup != this_play.pitcher:
this_lineup.active = False
self.session.add(this_lineup)
logger.info(f'Setting new pitcher on current play') logger.info(f'Setting new pitcher on current play')
this_play.pitcher = human_rp_lineup this_play.pitcher = human_rp_lineup
self.session.add(this_play) self.session.add(this_play)
@ -262,6 +262,77 @@ class SelectReliefPitcher(discord.ui.Select):
log_exception(e, 'Couldn\'t clean up after selecting rp') log_exception(e, 'Couldn\'t clean up after selecting rp')
class SelectDefensiveChange(discord.ui.Select):
def __init__(self, this_game: Game, this_team: Team, new_position: DEFENSE_NO_PITCHER_LITERAL, session: Session, responders: list[discord.User] = None, custom_id = MISSING, placeholder: str | None = None, options: List[SelectOption] = ...):
logger.info(f'Inside SelectDefensiveChange init function')
self.game = this_game
self.team = this_team
self.new_position = new_position
self.session = session
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(
content=random_insult(),
ephemeral=True,
delete_after=5
)
await interaction.response.defer(thinking=True)
logger.info(f'SelectDefensiveChange - selection: {self.values[0]}')
this_play = self.game.current_play_or_none(self.session)
if this_play is None:
log_exception(PlayNotFoundException, 'Could not find current play to make defensive change.')
# Get Human Defender card
human_defender_lineup = self.session.get(Lineup, self.values[0])
if human_defender_lineup is None:
log_exception(LineupsMissingException, f'Lineup ID {self.values[0]} not found')
if human_defender_lineup.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_defender_lineup.player.name} and belongs to {human_defender_lineup.team.sname}. Will you double check that?'
)
return
try:
other_defender = get_one_lineup(
session=self.session,
this_game=self.game,
this_team=self.team,
active=True,
position=position_name_to_abbrev(self.new_position)
)
logger.info(f'Existing defender found at {self.new_position}: {other_defender}')
except Exception as e:
logger.info(f'No existing defender found at {self.new_position}')
other_defender = None
human_defender_lineup.position = position_name_to_abbrev(self.new_position)
self.session.add(human_defender_lineup)
logger.info(f'Committing changes')
try:
self.session.commit()
except Exception as e:
log_exception(e, 'Couldn\'t commit database changes')
try:
logger.info(f'Responding to player')
await interaction.edit_original_response(
content=f'**{human_defender_lineup.player.name_with_desc}** has moved to {self.new_position} for the {human_defender_lineup.team.lname}!\n\nRun `/substitute` to make any other moves and `/gamestate` if you are ready to continue.',
view=None
)
except Exception as e:
log_exception(e, 'Couldn\'t clean up after selecting defender')
if other_defender:
await interaction.channel.send(f'FYI - I see {other_defender.player.name} was previously at {self.new_position}. Do not forget to move them or sub them out!')
@log_errors @log_errors
class SelectSubPosition(discord.ui.Select): class SelectSubPosition(discord.ui.Select):
def __init__(self, session: Session, this_lineup: Lineup, custom_id = MISSING, placeholder = None, options: List[SelectOption] = ..., responders: list[discord.User] = None): def __init__(self, session: Session, this_lineup: Lineup, custom_id = MISSING, placeholder = None, options: List[SelectOption] = ..., responders: list[discord.User] = None):