paper-dynasty-discord/command_logic/logic_gameplay.py
Cal Corum bfd72ae0f5 Update logging to RotatingFileHandler
Add auto game end
Calculate stats and decisions
Support raising instantiated exceptions
2024-11-09 23:14:54 -06:00

1463 lines
55 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 api_calls import db_delete, db_get, db_post
from exceptions import *
from helpers import DEFENSE_LITERAL, get_channel
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_db_ready_decisions, get_db_ready_plays, 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 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
logger = logging.getLogger('discord_app')
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
# print(f'get_wpa: new_rd = {new_rd} / old_rd = {old_rd}')
if (next_play.inning_num >= 9 and new_rd > 0 and next_play.inning_half == 'bot') or (next_play.inning_num > 9 and new_rd > 0 and next_play.is_new_inning):
# print(f'manually setting new_win_ex to 1.0')
new_win_ex = 1.0
else:
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
# print(f'new_win_ex = {new_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
# print(f'old_win_ex = {old_win_ex}')
wpa = float(round(new_win_ex - old_win_ex, 3))
# print(f'final wpa: {wpa}')
if this_play.inning_half == 'top':
return wpa * -1.0
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:
logger.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:
this_play.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:
this_play.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,
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]:
logger.debug(f'sheets: {sheets}')
this_sheet = sheets.open_by_key(this_team.gsheet)
logger.debug(f'this_sheet: {this_sheet}')
r_sheet = this_sheet.worksheet_by_title('My Rosters')
logger.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}'
logger.debug(f'l_range: {l_range}')
raw_cells = r_sheet.range(l_range)
logger.debug(f'raw_cells: {raw_cells}')
try:
lineup_cells = [(row[0].value, int(row[1].value)) for row in raw_cells]
logger.debug(f'lineup_cells: {lineup_cells}')
except ValueError as e:
logger.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)
logger.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:
logger.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:
logger.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:
logger.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:
logger.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
"""
logger.info(f'Advancing runners {num_bases} bases in game {this_play.game.id}')
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
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
logger.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:
logger.debug(f'calling of embed')
await show_outfield_cards(session, interaction, this_play)
logger.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:
logger.debug(f'calling of embed')
await show_outfield_cards(session, interaction, this_play)
logger.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)
logger.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)
logger.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:
logger.info(f'Lead runner is thrown out.')
this_play.outs += 1
# Lead runner is safe
else:
logger.info(f'Lead runner is safe.')
if this_play.on_second == lead_runner:
logger.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:
logger.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:
logger.info(f'Lead runner is thrown out.')
this_play.outs += 1
if this_play.on_second == lead_runner:
logger.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:
logger.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.run = 1, 1, 4, 1
this_play.bphr = 1 if homerun_type == 'ballpark' else 0
this_play = advance_runners(session, this_play, num_bases=4)
this_play.rbi += 1
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]
logger.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)
logger.info(f'Initialized play: {this_play.id}')
except PlayInitException:
this_play = activate_last_play(session, this_game)
logger.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)
def is_game_over(this_play: Play) -> bool:
print(f'1: ')
if this_play.inning_num < 9 and (abs(this_play.away_score - this_play.home_score) < 10):
return False
if abs(this_play.away_score - this_play.home_score) >= 10:
if ((this_play.home_score - this_play.away_score) >= 10) and this_play.inning_half == 'bot':
return True
elif ((this_play.away_score - this_play.home_score) >= 10) and this_play.is_new_inning and this_play.inning_half == 'top':
return True
if this_play.inning_num > 9 and this_play.inning_half == 'top' and this_play.is_new_inning and this_play.home_score != this_play.away_score:
return True
if this_play.inning_num >= 9 and this_play.inning_half == 'bot' and this_play.home_score > this_play.away_score:
return True
return False
async def get_game_summary_embed(session: Session, interaction: discord.Interaction, this_play: Play, db_game_id: int, winning_team: Team, losing_team: Team, num_potg: int = 1, num_poop: int = 0):
game_summary = await db_get(f'plays/game-summary/{db_game_id}', params=[('tp_max', num_potg)])
this_game = this_play.game
game_embed = winning_team.embed
game_embed.title = f'{this_game.away_team.lname} {this_play.away_score} @ {this_play.home_score} {this_game.home_team.lname} - F/{this_play.inning_num}'
game_embed.add_field(
name='Location',
value=f'{interaction.guild.get_channel(this_game.channel_id).mention}'
)
game_embed.add_field(name='Game ID', value=f'{db_game_id}')
if this_game.game_type == 'major-league':
game_des = 'Major League'
elif this_game.game_type == 'minor-league':
game_des = 'Minor League'
elif this_game.game_type == 'hall-of-fame':
game_des = 'Hall of Fame'
elif this_game.game_type == 'flashback':
game_des = 'Flashback'
elif this_game.ranked:
game_des = 'Ranked'
elif 'gauntlet' in this_game.game_type:
game_des = 'Gauntlet'
else:
game_des = 'Unlimited'
game_embed.description = f'Score Report - {game_des}'
game_embed.add_field(
name='Box Score',
value=f'```\n'
f'Team | R | H | E |\n'
f'{this_game.away_team.abbrev.replace("Gauntlet-", ""): <4} | {game_summary["runs"]["away"]: >2} | '
f'{game_summary["hits"]["away"]: >2} | {game_summary["errors"]["away"]: >2} |\n'
f'{this_game.home_team.abbrev.replace("Gauntlet-", ""): <4} | {game_summary["runs"]["home"]: >2} | '
f'{game_summary["hits"]["home"]: >2} | {game_summary["errors"]["home"]: >2} |\n'
f'\n```',
inline=False
)
logger.info(f'getting top players string')
potg_string = ''
for tp in game_summary['top-players']:
player_name = f'{get_player_name_from_dict(tp['player'])}'
potg_line = f'{player_name} - '
if 'hr' in tp:
potg_line += f'{tp["hit"]}-{tp["ab"]}'
if tp['hr'] > 0:
num = f'{tp["hr"]} ' if tp["hr"] > 1 else ""
potg_line += f', {num}HR'
if tp['triple'] > 0:
num = f'{tp["triple"]} ' if tp["triple"] > 1 else ""
potg_line += f', {num}3B'
if tp['double'] > 0:
num = f'{tp["double"]} ' if tp["double"] > 1 else ""
potg_line += f', {num}2B'
if tp['run'] > 0:
potg_line += f', {tp["run"]} R'
if tp['rbi'] > 0:
potg_line += f', {tp["rbi"]} RBI'
else:
potg_line = f'{player_name} - {tp["ip"]} IP, {tp["run"]} R'
if tp['run'] != tp['e_run']:
potg_line += f' ({tp["e_run"]} ER)'
potg_line += f', {tp["hit"]} H, {tp["so"]} K'
potg_line += f', {tp["re24"]:.2f} re24\n'
potg_string += potg_line
game_embed.add_field(
name='Players of the Game',
value=potg_string,
inline=False
)
pit_string = f'Win: {game_summary["pitchers"]["win"]["p_name"]}\nLoss: {game_summary["pitchers"]["loss"]["p_name"]}\n'
hold_string = None
for player in game_summary['pitchers']['holds']:
player_name = f'{get_player_name_from_dict(player)}'
if hold_string is None:
hold_string = f'Holds: {player_name}'
else:
hold_string += f', {player_name}'
if hold_string is not None:
pit_string += f'{hold_string}\n'
if game_summary['pitchers']['save'] is not None:
player_name = f'{get_player_name_from_dict(game_summary["pitchers"]["save"])}'
pit_string += f'Save: {player_name}'
game_embed.add_field(
name=f'Pitching',
value=pit_string,
)
def name_list(raw_list: list) -> str:
logger.info(f'raw_list: {raw_list}')
player_dict = {}
for x in raw_list:
if x['player_id'] not in player_dict:
player_dict[x['player_id']] = x
data_dict = {}
for x in raw_list:
if x['player_id'] not in data_dict:
data_dict[x['player_id']] = 1
else:
data_dict[x['player_id']] += 1
r_string = ''
logger.info(f'players: {player_dict} / data: {data_dict}')
first = True
for p_id in data_dict:
r_string += f'{", " if not first else ""}{player_dict[p_id]["p_name"]}'
if data_dict[p_id] > 1:
r_string += f' {data_dict[p_id]}'
first = False
return r_string
logger.info(f'getting running string')
if len(game_summary['running']['sb']) + len(game_summary['running']['csc']) > 0:
run_string = ''
if len(game_summary['running']['sb']) > 0:
run_string += f'SB: {name_list(game_summary["running"]["sb"])}\n'
if len(game_summary['running']['csc']) > 0:
run_string += f'CSc: {name_list(game_summary["running"]["csc"])}'
game_embed.add_field(
name=f'Baserunning',
value=run_string
)
logger.info(f'getting xbh string')
if len(game_summary['xbh']['2b']) + len(game_summary['xbh']['3b']) + len(game_summary['xbh']['hr']) > 0:
bat_string = ''
if len(game_summary['xbh']['2b']) > 0:
bat_string += f'2B: {name_list(game_summary["xbh"]["2b"])}\n'
if len(game_summary['xbh']['3b']) > 0:
bat_string += f'3B: {name_list(game_summary["xbh"]["3b"])}\n'
if len(game_summary['xbh']['hr']) > 0:
bat_string += f'HR: {name_list(game_summary["xbh"]["hr"])}\n'
else:
bat_string = 'Oops! All bitches! No XBH from either team.'
game_embed.add_field(
name='Batting',
value=bat_string,
inline=False
)
return game_embed
async def complete_game(session: Session, interaction: discord.Interaction, this_play: Play):
# if interaction is not None:
# salutation = await interaction.channel.send('GGs, I\'ll tally this game up...')
# Add button with {winning_team} wins! and another with "Roll Back"
this_game = this_play.game
async def roll_back(db_game_id: int, game: bool = True, plays: bool = False, decisions: bool = False):
if decisions:
try:
await db_delete('decisions/game', object_id=db_game_id)
except DatabaseError as e:
logger.warning(f'Could not delete decisions for game {db_game_id}: {e}')
if plays:
try:
await db_delete('plays/game', object_id=db_game_id)
except DatabaseError as e:
logger.warning(f'Could not delete plays for game {db_game_id}: {e}')
if game:
try:
await db_delete('games', object_id=db_game_id)
except DatabaseError as e:
logger.warning(f'Could not delete game {db_game_id}: {e}')
# Post completed game to API
game_data = this_game.model_dump()
game_data['home_team_ranking'] = this_game.home_team.ranking
game_data['away_team_ranking'] = this_game.away_team.ranking
game_data['home_team_value'] = this_game.home_team.team_value
game_data['away_team_value'] = this_game.away_team.team_value
game_data['away_score'] = this_play.away_score
game_data['home_score'] = this_play.home_score
winning_team = this_game.home_team if this_play.home_score > this_play.away_score else this_game.away_team
losing_team = this_game.home_team if this_play.away_score > this_play.home_score else this_game.away_team
try:
db_game = await db_post('games', payload=game_data)
db_ready_plays = get_db_ready_plays(session, this_game, db_game['id'])
db_ready_decisions = get_db_ready_decisions(session, this_game, db_game['id'])
except Exception as e:
await roll_back(db_game['id'])
log_exception(e, msg='Unable to post game to API, rolling back')
# Post game stats to API
try:
resp = await db_post('plays', payload=db_ready_plays)
except Exception as e:
await roll_back(db_game['id'], plays=True)
log_exception(e, msg='Unable to post plays to API, rolling back')
if len(resp) > 0:
pass
try:
resp = await db_post('decisions', payload={'decisions': db_ready_decisions})
except Exception as e:
await roll_back(db_game['id'], plays=True, decisions=True)
log_exception(e, msg='Unable to post decisions to API, rolling back')
if len(resp) > 0:
pass
# Post game rewards (gauntlet and main team)
try:
win_reward, loss_reward = await post_game_rewards(
session,
winning_team=winning_team,
losing_team=losing_team,
this_game=this_game
)
except Exception as e:
await roll_back(db_game['id'], plays=True, decisions=True)
log_exception(e, msg='Error while posting game rewards')
session.delete(this_play)
session.commit()
# Pull game summary for embed
summary_embed = await get_game_summary_embed(
session,
interaction,
this_play,
db_game['id'],
winning_team=winning_team,
losing_team=losing_team,
num_potg=3,
num_poop=1
)
summary_embed.add_field(
name=f'{winning_team.abbrev} Rewards',
value=win_reward
)
summary_embed.add_field(
name=f'{losing_team.abbrev} Rewards',
value=loss_reward
)
summary_embed.add_field(
name='Highlights',
value=f'Please share the highlights in {get_channel(interaction, "pd-news-ticker").mention}!',
inline=False
)
# Create and post game summary to game channel and pd-network-news
news_ticker = get_channel(interaction, 'pd-network-news')
if news_ticker is not None:
await news_ticker.send(content=None, embed=summary_embed)
await interaction.channel.send(content=None, embed=summary_embed)
await interaction.edit_original_response(content=None, embed=summary_embed)
game_id = this_game.id
this_game.active = False
session.add(this_game)
session.commit()
logger.info(f'Just ended game {game_id}')