paper-dynasty-discord/command_logic/logic_gameplay.py
Cal Corum c3418c4dfd 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
2024-11-09 00:48:13 -06:00

1152 lines
44 KiB
Python

import asyncio
import logging
import discord
import pandas as pd
from sqlmodel import Session, select, func
from sqlalchemy import delete
from typing import Literal
from exceptions import *
from helpers import DEFENSE_LITERAL
from in_game.game_helpers import legal_check
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_sorted_lineups, get_team_or_none, get_players_last_pa
from utilities.buttons import ButtonOptions, Confirm, ask_confirm
from utilities.dropdown import DropdownOptions, DropdownView, SelectViewDefense
from utilities.embeds import image_embed
from utilities.pages import Pagination
WPA_DF = pd.read_csv(f'storage/wpa_data.csv').set_index('index')
def get_obc(on_first = None, on_second = None, on_third = None) -> int:
if on_third is not None:
if on_second is not None:
if on_first is not None:
obc = 7
else:
obc = 6
elif on_first is not None:
obc = 5
else:
obc = 3
elif on_second is not None:
if on_first is not None:
obc = 4
else:
obc = 2
elif on_first is not None:
obc = 1
else:
obc = 0
return obc
def get_re24(this_play: Play, runs_scored: int, new_obc: int, new_starting_outs: int) -> float:
re_data = {
0: [0.457, 0.231, 0.077],
1: [0.793, 0.438, 0.171],
2: [1.064, 0.596, 0.259],
4: [1.373, 0.772, 0.351],
3: [1.340, 0.874, 0.287],
5: [1.687, 1.042, 0.406],
6: [1.973, 1.311, 0.448],
7: [2.295, 1.440, 0.618]
}
start_re24 = re_data[this_play.on_base_code][this_play.starting_outs]
end_re24 = 0 if this_play.starting_outs + this_play.outs > 2 else re_data[new_obc][new_starting_outs]
return round(end_re24 - start_re24 + runs_scored, 3)
def get_wpa(this_play: Play, next_play: Play):
"""
Returns wpa relative to batting team of this_play. Negative value if bad play, positive value if good play.
"""
new_rd = next_play.home_score - next_play.away_score
if new_rd > 6:
new_rd = 6
elif new_rd < -6:
new_rd = -6
old_rd = this_play.home_score - this_play.away_score
if old_rd > 6:
old_rd = 6
elif old_rd < -6:
old_rd = -6
new_win_ex = WPA_DF.loc[f'{next_play.inning_half}_{next_play.inning_num}_{next_play.starting_outs}_out_{next_play.on_base_code}_obc_{new_rd}_home_run_diff'].home_win_ex
old_win_ex = WPA_DF.loc[f'{this_play.inning_half}_{this_play.inning_num}_{this_play.starting_outs}_out_{this_play.on_base_code}_obc_{old_rd}_home_run_diff'].home_win_ex
wpa = round(new_win_ex - old_win_ex, 3)
if this_play.inning_half == 'top':
return wpa * -1
return wpa
def complete_play(session:Session, this_play: Play):
"""
Commits this_play and new_play
"""
nso = this_play.starting_outs + this_play.outs
runs_scored = 0
on_first, on_second, on_third = None, None, None
is_go_ahead = False
if nso >= 3:
switch_sides = True
obc = 0
nso = 0
nih = 'bot' if this_play.inning_half.lower() == 'top' else 'top'
away_score = this_play.away_score
home_score = this_play.home_score
try:
opponent_play = get_last_team_play(session, this_play.game, this_play.pitcher.team)
nbo = opponent_play.batting_order + 1
except PlayNotFoundException as e:
logging.info(f'logic_gameplay - complete_play - No last play found for {this_play.pitcher.team.sname}, setting upcoming batting order to 1')
nbo = 1
finally:
new_batter_team = this_play.game.away_team if nih == 'top' else this_play.game.home_team
new_pitcher_team = this_play.game.away_team if nih == 'bot' else this_play.game.home_team
inning = this_play.inning_num if nih == 'bot' else this_play.inning_num + 1
else:
switch_sides = False
nbo = this_play.batting_order + 1 if this_play.pa == 1 else this_play.batting_order
nih = this_play.inning_half
new_batter_team = this_play.batter.team
new_pitcher_team = this_play.pitcher.team
inning = this_play.inning_num
for this_runner, runner_dest in [
(this_play.batter, this_play.batter_final), (this_play.on_first, this_play.on_first_final), (this_play.on_second, this_play.on_second_final), (this_play.on_third, this_play.on_third_final)
]:
if runner_dest is not None:
if runner_dest == 1:
if on_first is not None:
log_exception(ValueError, f'Cannot place {this_runner.player.name} on first; {on_first.player.name} is already placed there')
on_first = this_runner
elif runner_dest == 2:
if on_second is not None:
log_exception(ValueError, f'Cannot place {this_runner.player.name} on second; {on_second.player.name} is already placed there')
on_second = this_runner
elif runner_dest == 3:
if on_third is not None:
log_exception(ValueError, f'Cannot place {this_runner.player.name} on third; {on_third.player.name} is already placed there')
on_third = this_runner
elif runner_dest == 4:
runs_scored += 1
if this_play.inning_half == 'top':
away_score = this_play.away_score + runs_scored
home_score = this_play.home_score
if runs_scored > 0 and this_play.away_score <= this_play.home_score and away_score > home_score:
is_go_ahead = True
else:
away_score = this_play.away_score
home_score = this_play.home_score + runs_scored
if runs_scored > 0 and this_play.home_score <= this_play.away_score and home_score > away_score:
is_go_ahead = True
obc = get_obc(on_first, on_second, on_third)
this_play.re24 = get_re24(this_play, runs_scored, new_obc=obc, new_starting_outs=nso)
if nbo > 9:
nbo = 1
new_batter = get_one_lineup(session, this_play.game, new_batter_team, batting_order=nbo)
new_play = Play(
game=this_play.game,
play_num=this_play.play_num + 1,
batting_order=nbo,
inning_half=nih,
inning_num=inning,
starting_outs=nso,
on_base_code=obc,
away_score=away_score,
home_score=home_score,
batter=new_batter,
batter_pos=new_batter.position,
pitcher=get_one_lineup(session, this_play.game, new_pitcher_team, position='P'),
catcher=get_one_lineup(session, this_play.game, new_pitcher_team, position='C'),
is_new_inning=switch_sides,
is_tied=away_score == home_score,
is_go_ahead=is_go_ahead,
on_first=on_first,
on_second=on_second,
on_third=on_third,
managerai=this_play.managerai,
re24=get_re24(this_play, runs_scored, new_obc=obc, new_starting_outs=nso)
)
this_play.wpa = get_wpa(this_play, new_play)
this_play.locked = False
this_play.complete = True
session.add(this_play)
session.add(new_play)
session.commit()
session.refresh(new_play)
return new_play
async def get_lineups_from_sheets(session: Session, sheets, this_game: Game, this_team: Team, lineup_num: int, roster_num: int) -> list[Lineup]:
logging.debug(f'sheets: {sheets}')
this_sheet = sheets.open_by_key(this_team.gsheet)
logging.debug(f'this_sheet: {this_sheet}')
r_sheet = this_sheet.worksheet_by_title('My Rosters')
logging.debug(f'r_sheet: {r_sheet}')
if lineup_num == 1:
row_start = 9
row_end = 17
else:
row_start = 18
row_end = 26
if roster_num == 1:
l_range = f'H{row_start}:I{row_end}'
elif roster_num == 2:
l_range = f'J{row_start}:K{row_end}'
else:
l_range = f'L{row_start}:M{row_end}'
logging.debug(f'l_range: {l_range}')
raw_cells = r_sheet.range(l_range)
logging.debug(f'raw_cells: {raw_cells}')
try:
lineup_cells = [(row[0].value, int(row[1].value)) for row in raw_cells]
logging.debug(f'lineup_cells: {lineup_cells}')
except ValueError as e:
logging.error(f'Could not pull roster for {this_team.abbrev}: {e}')
raise ValueError(f'Uh oh. Looks like your roster might not be saved. I am reading blanks when I try to get the card IDs')
all_lineups = []
all_pos = []
card_ids = []
for index, row in enumerate(lineup_cells):
if '' in row:
break
if row[0].upper() not in all_pos:
all_pos.append(row[0].upper())
else:
raise SyntaxError(f'You have more than one {row[0].upper()} in this lineup. Please update and set the lineup again.')
this_card = await get_card_or_none(session, card_id=int(row[1]))
if this_card is None:
raise LookupError(
f'Your {row[0].upper()} has a Card ID of {int(row[1])} and I cannot find that card. Did you sell it by chance? Or maybe you sold a duplicate and the bot sold the one you were using?'
)
if this_card.team_id != this_team.id:
raise SyntaxError(f'Easy there, champ. Looks like card ID {row[1]} belongs to the {this_card.team.lname}. Try again with only cards you own.')
card_id = row[1]
card_ids.append(str(card_id))
this_lineup = Lineup(
position=row[0].upper(),
batting_order=index + 1,
game=this_game,
team=this_team,
player=this_card.player,
card=this_card
)
all_lineups.append(this_lineup)
legal_data = await legal_check([card_ids], difficulty_name=this_game.game_type)
logging.debug(f'legal_data: {legal_data}')
if not legal_data['legal']:
raise CardLegalityException(f'The following cards appear to be illegal for this game mode:\n{legal_data["error_string"]}')
if len(all_lineups) != 9:
raise Exception(f'I was only able to pull in {len(all_lineups)} batters from Sheets. Please check your saved lineup and try again.')
return all_lineups
async def checks_log_interaction(session: Session, interaction: discord.Interaction, command_name: str) -> tuple[Game, Team, Play]:
"""
Commits this_play
"""
await interaction.response.defer(thinking=True)
this_game = get_channel_game_or_none(session, interaction.channel_id)
if this_game is None:
raise GameNotFoundException('I don\'t see an active game in this channel.')
owner_team = await get_team_or_none(session, gm_id=interaction.user.id)
if owner_team is None:
logging.exception(f'{command_name} command: No team found for GM ID {interaction.user.id}')
raise TeamNotFoundException(f'Do I know you? I cannot find your team.')
if 'gauntlet' in this_game.game_type:
gauntlet_abbrev = f'Gauntlet-{owner_team.abbrev}'
owner_team = await get_team_or_none(session, team_abbrev=gauntlet_abbrev)
if owner_team is None:
logging.exception(f'{command_name} command: No gauntlet team found with abbrev {gauntlet_abbrev}')
raise TeamNotFoundException(f'Hm, I was not able to find a gauntlet team for you.')
if not owner_team.id in [this_game.away_team_id, this_game.home_team_id]:
if interaction.user.id != 258104532423147520:
logging.exception(f'{interaction.user.display_name} tried to run a command in Game {this_game.id} when they aren\'t a GM in the game.')
raise TeamNotFoundException('Bruh. Only GMs of the active teams can log plays.')
else:
await interaction.channel.send(f'Cal is bypassing the GM check to run the {command_name} command')
this_play = this_game.current_play_or_none(session)
if this_play is None:
logging.error(f'{command_name} command: No play found for Game ID {this_game.id} - attempting to initialize play')
this_play = activate_last_play(session, this_game)
this_play.locked = True
session.add(this_play)
session.commit()
return this_game, owner_team, this_play
def log_run_scored(session: Session, runner: Lineup, this_play: Play, is_earned: bool = True):
"""
Commits last_ab
"""
last_ab = get_players_last_pa(session, lineup_member=runner)
last_ab.run = 1
errors = session.exec(select(func.count(Play.id)).where(
Play.inning_num == last_ab.inning_num, Play.inning_half == last_ab.inning_half, Play.error == 1
)).one()
outs = session.exec(select(func.sum(Play.outs)).where(
Play.inning_num == last_ab.inning_num, Play.inning_half == last_ab.inning_half
)).one()
if errors + outs + this_play.error >= 3:
is_earned = False
last_ab.e_run = 1 if is_earned else 0
session.add(last_ab)
session.commit()
return True
def advance_runners(session: Session, this_play: Play, num_bases: int, is_error: bool = False, only_forced: bool = False) -> Play:
"""
No commits
"""
this_play.rbi = 0
if num_bases == 0:
if this_play.on_first is not None:
this_play.on_first_final = 1
if this_play.on_second_id is not None:
this_play.on_second_final = 2
if this_play.on_third_id is not None:
this_play.on_third_final = 3
elif only_forced:
if not this_play.on_first:
if this_play.on_second:
this_play.on_second_final = 2
if this_play.on_third:
this_play.on_third_final = 3
return
if this_play.on_second:
if this_play.on_third:
if num_bases > 0:
this_play.on_third_final = 4
log_run_scored(session, this_play.on_third, this_play)
this_play.rbi += 1 if not is_error else 0
if num_bases > 1:
this_play.on_second_final = 4
log_run_scored(session, this_play.on_second, this_play)
this_play.rbi += 1 if not is_error else 0
elif num_bases == 1:
this_play.on_second_final = 3
else:
this_play.on_second_final = 2
else:
if this_play.on_third:
this_play.on_third_final = 3
if num_bases > 2:
this_play.on_first_final = 4
log_run_scored(session, this_play.on_first, this_play)
this_play.rbi += 1 if not is_error else 0
elif num_bases == 2:
this_play.on_first_final = 3
elif num_bases == 1:
this_play.on_first_final = 2
else:
this_play.on_first_final = 1
else:
if this_play.on_third:
if num_bases > 0:
this_play.on_third_final = 4
log_run_scored(session, this_play.on_third, this_play)
this_play.rbi += 1 if not is_error else 0
else:
this_play.on_third_final = 3
if this_play.on_second:
if num_bases > 1:
this_play.on_second_final = 4
log_run_scored(session, this_play.on_second, this_play)
this_play.rbi += 1 if not is_error else 0
elif num_bases == 1:
this_play.on_second_final = 3
else:
this_play.on_second_final = 2
if this_play.on_first:
if num_bases > 2:
this_play.on_first_final = 4
log_run_scored(session, this_play.on_first, this_play)
this_play.rbi += 1 if not is_error else 0
elif num_bases == 2:
this_play.on_first_final = 3
elif num_bases == 1:
this_play.on_first_final = 2
else:
this_play.on_first_final = 1
return this_play
async def show_outfield_cards(session: Session, interaction: discord.Interaction, this_play: Play) -> Lineup:
lf = get_one_lineup(session, this_game=this_play.game, this_team=this_play.pitcher.team, position='LF')
cf = get_one_lineup(session, this_game=this_play.game, this_team=this_play.pitcher.team, position='CF')
rf = get_one_lineup(session, this_game=this_play.game, this_team=this_play.pitcher.team, position='RF')
this_team = this_play.pitcher.team
logging.debug(f'lf: {lf.player.name_with_desc}\n\ncf: {cf.player.name_with_desc}\n\nrf: {rf.player.name_with_desc}\n\nteam: {this_team.lname}')
view = Pagination([interaction.user], timeout=10)
view.left_button.label = f'Left Fielder'
view.left_button.style = discord.ButtonStyle.secondary
lf_embed = image_embed(
image_url=lf.player.image,
title=f'{this_team.sname} LF',
color=this_team.color,
desc=lf.player.name,
author_name=this_team.lname,
author_icon=this_team.logo
)
view.cancel_button.label = f'Center Fielder'
view.cancel_button.style = discord.ButtonStyle.blurple
cf_embed = image_embed(
image_url=cf.player.image,
title=f'{this_team.sname} CF',
color=this_team.color,
desc=cf.player.name,
author_name=this_team.lname,
author_icon=this_team.logo
)
view.right_button.label = f'Right Fielder'
view.right_button.style = discord.ButtonStyle.secondary
rf_embed = image_embed(
image_url=rf.player.image,
title=f'{this_team.sname} RF',
color=this_team.color,
desc=rf.player.name,
author_name=this_team.lname,
author_icon=this_team.logo
)
page_num = 1
embeds = [lf_embed, cf_embed, rf_embed]
msg = await interaction.channel.send(embed=embeds[page_num], view=view)
await view.wait()
if view.value:
if view.value == 'left':
page_num = 0
if view.value == 'cancel':
page_num = 1
if view.value == 'right':
page_num = 2
else:
await msg.edit(content=None, embed=embeds[page_num], view=None)
view.value = None
if page_num == 0:
view.left_button.style = discord.ButtonStyle.blurple
view.cancel_button.style = discord.ButtonStyle.secondary
view.right_button.style = discord.ButtonStyle.secondary
if page_num == 1:
view.left_button.style = discord.ButtonStyle.secondary
view.cancel_button.style = discord.ButtonStyle.blurple
view.right_button.style = discord.ButtonStyle.secondary
if page_num == 2:
view.left_button.style = discord.ButtonStyle.secondary
view.cancel_button.style = discord.ButtonStyle.secondary
view.right_button.style = discord.ButtonStyle.blurple
view.left_button.disabled = True
view.cancel_button.disabled = True
view.right_button.disabled = True
await msg.edit(content=None, embed=embeds[page_num], view=view)
return [lf, cf, rf][page_num]
async def flyballs(session: Session, interaction: discord.Interaction, this_play: Play, flyball_type: Literal['a', 'ballpark', 'b', 'b?', 'c']) -> Play:
"""
Commits this_play
"""
this_game = this_play.game
num_outs = 1
if flyball_type == 'a':
this_play.pa, this_play.ab, this_play.outs = 1, 1, 1
if this_play.starting_outs < 2:
advance_runners(session, this_play, num_bases=1)
if this_play.on_third:
this_play.ab = 0
elif flyball_type == 'b' or flyball_type == 'ballpark':
this_play.pa, this_play.ab, this_play.outs = 1, 1, 1
this_play.bpfo = 1 if flyball_type == 'ballpark' else 0
advance_runners(session, this_play, num_bases=0)
if this_play.starting_outs < 2 and this_play.on_third:
this_play.ab = 0
this_play.rbi = 1
this_play.on_third_final = 4
log_run_scored(session, this_play.on_third, this_play)
if this_play.starting_outs < 2 and this_play.on_second:
logging.debug(f'calling of embed')
await show_outfield_cards(session, interaction, this_play)
logging.debug(f'done with of embed')
runner = this_play.on_second.player
view = Confirm(responders=[interaction.user], timeout=60, label_type='yes')
if this_play.ai_is_batting:
tag_resp = this_play.managerai.tag_from_second(session, this_game)
q_text = f'{runner.name} will attempt to advance to third if the safe range is **{tag_resp.min_safe}+**, are they going?'
else:
q_text = f'Is {runner.name} attempting to tag up to third?'
question = await interaction.channel.send(
content=q_text,
view=view
)
await view.wait()
if view.value:
await question.delete()
view = ButtonOptions(
responders=[interaction.user], timeout=60,
labels=['Tagged Up', 'Hold at 2nd', 'Out at 3rd', None, None]
)
question = await interaction.channel.send(
f'What was the result of {runner.name} tagging from second?', view=view
)
await view.wait()
if view.value:
await question.delete()
if view.value == 'Tagged Up':
this_play.on_second_final = 3
elif view.value == 'Out at 3rd':
num_outs += 1
this_play.on_second_final = None
this_play.outs = num_outs
else:
await question.delete()
else:
await question.delete()
elif flyball_type == 'b?':
this_play.pa, this_play.ab, this_play.outs = 1, 1, 1
if this_play.starting_outs < 2 and this_play.on_third:
logging.debug(f'calling of embed')
await show_outfield_cards(session, interaction, this_play)
logging.debug(f'done with of embed')
runner = this_play.on_second.player
view = Confirm(responders=[interaction.user], timeout=60, label_type='yes')
if this_play.ai_is_batting:
tag_resp = this_play.managerai.tag_from_second(session, this_game)
q_text = f'{runner.name} will attempt to advance home if the safe range is **{tag_resp.min_safe}+**, are they going?'
else:
q_text = f'Is {runner.name} attempting to tag up and go home?'
question = await interaction.channel.send(
content=q_text,
view=view
)
await view.wait()
if view.value:
await question.delete()
view = Confirm(responders=[interaction.user], timeout=60, label_type='yes')
question = await interaction.channel.send(
f'Was {runner.name} thrown out?', view=view
)
await view.wait()
if view.value:
await question.delete()
num_outs += 1
this_play.on_third_final = 99
this_play.outs = num_outs
else:
await question.delete()
this_play.ab = 0
this_play.rbi = 1
this_play.on_third_final = 4
log_run_scored(session, this_play.on_third, this_play)
else:
await question.delete()
elif flyball_type == 'c':
this_play.pa, this_play.ab, this_play.outs = 1, 1, 1
advance_runners(session, this_play, num_bases=0)
session.add(this_play)
session.commit()
session.refresh(this_play)
return this_play
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)
logging.info(f'throw from {outfielder.player.name_with_desc}')
def_team = this_play.pitcher.team
TO_BASE = {
2: 'to second',
3: 'to third',
4: 'home'
}
AT_BASE = {
2: 'at second',
3: 'at third',
4: 'at home'
}
# Either there is no AI team or the AI is pitching
if not this_game.ai_team or not this_play.ai_is_batting:
is_lead_running = await ask_confirm(
interaction=interaction,
question=f'Is **{lead_runner.player.name}** being sent {TO_BASE[lead_base]}?',
label_type='yes'
)
if is_lead_running:
throw_resp = None
if this_game.ai_team:
throw_resp = this_play.managerai.throw_at_uncapped(session, this_game)
logging.info(f'throw_resp: {throw_resp}')
if throw_resp.cutoff:
await interaction.channel.send(f'The {def_team.sname} will cut off the throw {TO_BASE[lead_base]}')
if this_play.on_second == lead_runner:
this_play.rbi += 1
this_play.on_second_final = 4
log_run_scored(session, lead_runner, this_play)
else:
this_play.on_first_final = 3
await asyncio.sleep(1)
return this_play
else:
throw_for_lead = await ask_confirm(
interaction=interaction,
question=f'Is the defense throwing {TO_BASE[lead_base]} for {lead_runner.player.name}?',
label_type='yes'
)
# Human defense is cutting off the throw
if not throw_for_lead:
await question.delete()
if this_play.on_second == lead_runner:
this_play.rbi += 1
this_play.on_second_final = 4
log_run_scored(session, lead_runner, this_play)
return this_play
# Human runner is advancing, defense is throwing
trail_advancing = await ask_confirm(
interaction=interaction,
question=f'Is **{trail_runner.player.name}** being sent {TO_BASE[trail_base]} as the trail runner?',
label_type='yes'
)
# Trail runner is advancing
if trail_advancing:
view = Confirm(responders=[interaction.user], timeout=60, label_type='yes')
view.confirm.label = 'Home Plate' if lead_base == 4 else 'Third Base'
view.cancel.label = 'Third Base' if trail_base == 3 else 'Second Base'
ai_throw_lead = False
if this_game.ai_team:
if throw_resp.at_trail_runner:
question = await interaction.channel.send(
f'The {def_team.sname} will throw for the trail runner if both:\n- {trail_runner.player.name}\'s safe range is {throw_resp.trail_max_safe} or lower\n- {trail_runner.player.name}\'s safe range is lower than {lead_runner.player.name}\'s by at least {abs(throw_resp.trail_max_safe_delta)}.\n\nIs the throw going {TO_BASE[lead_base]} or {TO_BASE[trail_base]}?',
view=view
)
else:
await interaction.channel.send(f'**{outfielder.player.name}** will throw {TO_BASE[lead_base]}!')
ai_throw_lead = True
else:
question = await interaction.channel.send(
f'Is the throw going {TO_BASE[lead_base]} or {TO_BASE[trail_base]}?',
view=view
)
if not ai_throw_lead:
await view.wait()
elif ai_throw_lead:
view.value = True
# Throw is going to lead runner
if view.value:
try:
await question.delete()
except (discord.NotFound, UnboundLocalError):
pass
if this_play.on_first == trail_runner:
this_play.on_first_final += 1
elif this_play.batter == trail_runner:
this_play.batter_final += 1
else:
log_exception(LineupsMissingException, f'Could not find trail runner to advance')
# Throw is going to trail runner
else:
try:
await question.delete()
except (discord.NotFound, UnboundLocalError):
pass
runner_thrown_out = await ask_confirm(
interaction=interaction,
question='Was **{trail_runner.player.name}** thrown out {AT_BASE[trail_base]}?',
label_type='yes'
)
# Trail runner is thrown out
if runner_thrown_out:
# Log out on play
this_play.outs += 1
# Remove trail runner
if this_play.on_first == trail_runner:
this_play.on_first_final = None
else:
this_play.batter_final = None
# Advance lead runner extra base
if this_play.on_second == lead_runner:
this_play.rbi += 1
this_play.on_second_final = 4
log_run_scored(session, lead_runner, this_play)
elif this_play.on_first == lead_runner:
this_play.on_first_final += 1
if this_play.on_first_final > 3:
this_play.rbi += 1
log_run_scored(session, lead_runner, this_play)
return this_play
# Ball is going to lead base, ask if safe
runner_thrown_out = await ask_confirm(
interaction=interaction,
question=f'Was **{lead_runner.player.name}** thrown out {AT_BASE[lead_base]}?',
label_type='yes'
)
# Lead runner is thrown out
if runner_thrown_out:
logging.info(f'Lead runner is thrown out.')
this_play.outs += 1
# Lead runner is safe
else:
logging.info(f'Lead runner is safe.')
if this_play.on_second == lead_runner:
logging.info(f'setting lead runner on_second_final')
this_play.on_second_final = None if runner_thrown_out else lead_base
elif this_play.on_first == lead_runner:
logging.info(f'setting lead runner on_first')
this_play.on_first_final = None if runner_thrown_out else lead_base
else:
log_exception(LineupsMissingException, f'Could not find lead runner to set final destination')
# Human lead runner is not advancing
else:
return this_play
elif this_play.ai_is_batting:
run_resp = this_play.managerai.uncapped_advance(session, this_game, lead_base, trail_base)
is_lead_running = await ask_confirm(
interaction=interaction,
question=f'**{lead_runner.player.name}** will advance {TO_BASE[lead_base]} if the safe range is {run_resp.min_safe} or higher.\n\nIs **{lead_runner.player.name}** attempting to advance?',
label_type='yes'
)
if not is_lead_running:
return this_play
is_defense_throwing = await ask_confirm(
interaction=interaction,
question=f'Is the defense throwing {TO_BASE[lead_base]} for {lead_runner.player.name}?',
label_type='yes'
)
# Human defense is throwing for lead runner
if not is_defense_throwing:
if this_play.on_second == lead_runner:
this_play.rbi += 1
this_play.on_second_final = 4
log_run_scored(session, lead_runner, this_play)
elif this_play.on_first == lead_runner:
this_play.on_first_final = 3
return this_play
# Human throw is not being cut off
if run_resp.send_trail:
await interaction.channel.send(
f'**{trail_runner.player.name}** is advancing {TO_BASE[trail_base]} as the trail runner!',
)
is_throwing_lead = await ask_confirm(
interaction=interaction,
question=f'Is the throw going {TO_BASE[lead_base]} or {TO_BASE[trail_base]}?',
label_type='yes',
custom_confirm_label='Home Plate' if lead_base == 4 else 'Third Base',
custom_cancel_label='Third Base' if trail_base == 3 else 'Second Base'
)
# Trail runner advances, throwing for lead runner
if is_throwing_lead:
if this_play.on_first == trail_runner:
this_play.on_first_final += 1
elif this_play.batter == trail_runner:
this_play.batter_final += 1
else:
log_exception(LineupsMissingException, f'Could not find trail runner to advance')
# Throw is going to trail runner
else:
is_trail_out = await ask_confirm(
interaction=interaction,
question=f'Was **{trail_runner.player.name}** thrown out {AT_BASE[trail_base]}?',
label_type='yes'
)
if is_trail_out:
# Log out on play
this_play.outs += 1
# Remove trail runner
if this_play.on_first == trail_runner:
this_play.on_first_final = None
else:
this_play.batter_final = None
# Advance lead runner extra base
if this_play.on_second == lead_runner:
this_play.rbi += 1
this_play.on_second_final = 4
log_run_scored(session, lead_runner, this_play)
elif this_play.on_first == lead_runner:
this_play.on_first_final += 1
if this_play.on_first_final > 3:
this_play.rbi += 1
log_run_scored(session, lead_runner, this_play)
return this_play
# Ball is going to lead base, ask if safe
is_lead_out = await ask_confirm(
interaction=interaction,
question=f'Was **{lead_runner.player.name}** thrown out {AT_BASE[lead_base]}?',
label_type='yes',
)
# Lead runner is thrown out
if is_lead_out:
logging.info(f'Lead runner is thrown out.')
this_play.outs += 1
if this_play.on_second == lead_runner:
logging.info(f'setting lead runner on_second_final')
this_play.on_second_final = None if is_lead_out else lead_base
elif this_play.on_first == lead_runner:
logging.info(f'setting lead runner on_first')
this_play.on_first_final = None if is_lead_out else lead_base
else:
log_exception(LineupsMissingException, f'Could not find lead runner to set final destination')
return this_play
async def singles(session: Session, interaction: discord.Interaction, this_play: Play, single_type: Literal['*', '**', 'ballpark', 'uncapped']) -> Play:
"""
Commits this_play
"""
this_play.hit, this_play.batter_final = 1, 1
if single_type == '**':
advance_runners(session, this_play, num_bases=2)
elif single_type in ['*', 'ballpark']:
advance_runners(session, this_play, num_bases=1)
this_play.bp1b = 1 if single_type == 'ballpark' else 0
elif single_type == 'uncapped':
advance_runners(session, this_play, 1)
if this_play.on_base_code in [1, 2, 4, 5, 6, 7]:
if this_play.on_second:
lead_runner = this_play.on_second
lead_base = 4
if this_play.on_first:
trail_runner = this_play.on_first
trail_base = 3
else:
trail_runner = this_play.batter
trail_base = 2
else:
lead_runner = this_play.on_first
lead_base = 3
trail_runner = this_play.batter
trail_base = 2
this_play = await check_uncapped_advance(session, interaction, this_play, lead_runner, lead_base, trail_runner, trail_base)
session.add(this_play)
session.commit()
session.refresh(this_play)
return this_play
async def doubles(session: Session, interaction: discord.Interaction, this_play: Play, double_type: Literal['**', '***', 'uncapped']) -> Play:
"""
Commits this_play
"""
this_play.hit, this_play.double, this_play.batter_final = 1, 1, 2
if double_type == '**':
this_play = advance_runners(session, this_play, num_bases=2)
elif double_type == '***':
this_play = advance_runners(session, this_play, num_bases=3)
elif double_type == 'uncapped':
this_play = advance_runners(session, this_play, num_bases=2)
if this_play.on_first:
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.commit()
session.refresh(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)