paper-dynasty-discord/command_logic/logic_gameplay.py
Cal Corum 724b8922f2 Update gauntlet get SP for new objects
Handle gamestates without full lineups
Added /set command for lineup and SP
Fixed uncapped hit bugs
Added league_name property to Games
Fix get_team for gauntlets
Fixed SelectSP dropdown bug
2024-12-27 16:12:25 -06:00

3537 lines
143 KiB
Python

import asyncio
import copy
import logging
import discord
from discord import SelectOption
from discord.app_commands import Choice
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 dice import DTwentyRoll, d_twenty_roll, frame_plate_check, sa_fielding_roll
from exceptions import *
from helpers import DEFENSE_LITERAL, SBA_COLOR, get_channel
from in_game.game_helpers import PUBLIC_FIELDS_CATEGORY_NAME, legal_check
from in_game.gameplay_models import BattingCard, Game, Lineup, PositionRating, RosterLink, Team, Play
from in_game.gameplay_queries import get_available_batters, get_batter_card, get_batting_statline, get_pitching_statline, 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 utilities.buttons import ButtonOptions, Confirm, ask_confirm
from utilities.dropdown import DropdownView, SelectBatterSub, SelectStartingPitcher, 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')
TO_BASE = {
2: 'to second',
3: 'to third',
4: 'home'
}
AT_BASE = {
2: 'at second',
3: 'at third',
4: 'at home'
}
RANGE_CHECKS = {
1: 3,
2: 7,
3: 11,
4: 15,
5: 19
}
async def get_scorebug_embed(session: Session, this_game: Game, full_length: bool = True, classic: bool = True) -> discord.Embed:
gt_string = ' - Unlimited'
if this_game.game_type == 'minor-league':
gt_string = ' - Minor League'
elif this_game.game_type == 'major-league':
gt_string = ' - Major League'
elif this_game.game_type == 'hall-of-fame':
gt_string = ' - Hall of Fame'
elif 'gauntlet' in this_game.game_type:
gt_string = ' - Gauntlet'
elif 'flashback' in this_game.game_type:
gt_string = ' - Flashback'
elif 'exhibition' in this_game.game_type:
gt_string = ' - Exhibition'
logger.info(f'get_scorebug_embed - this_game: {this_game} / gt_string: {gt_string}')
embed = discord.Embed(
title=f'{this_game.away_team.sname} @ {this_game.home_team.sname}{gt_string}',
color=int(SBA_COLOR, 16)
)
curr_play = this_game.current_play_or_none(session)
if curr_play is None:
try:
curr_play = this_game.initialize_play(session)
except LineupsMissingException as e:
logger.debug(f'get_scorebug_embed - Could not initialize play')
if curr_play is not None:
embed.add_field(
name='Game State',
value=curr_play.scorebug_ascii,
inline=False
)
logger.info(f'curr_play: {curr_play}')
def steal_string(batting_card: BattingCard) -> str:
steal_string = '-/- (---)'
if batting_card.steal_jump > 0:
jump_chances = round(batting_card.steal_jump * 36)
if jump_chances == 6:
good_jump = 7
elif jump_chances == 5:
good_jump = 6
elif jump_chances == 4:
good_jump = 5
elif jump_chances == 3:
good_jump = 4
elif jump_chances == 2:
good_jump = 3
elif jump_chances == 1:
good_jump = 2
elif jump_chances == 7:
good_jump = '4,5'
elif jump_chances == 8:
good_jump = '4,6'
elif jump_chances == 9:
good_jump = '3-5'
elif jump_chances == 10:
good_jump = '2-5'
elif jump_chances == 11:
good_jump = '6,7'
elif jump_chances == 12:
good_jump = '4-6'
elif jump_chances == 13:
good_jump = '2,4-6'
elif jump_chances == 14:
good_jump = '3-6'
elif jump_chances == 15:
good_jump = '2-6'
elif jump_chances == 16:
good_jump = '2,5-6'
elif jump_chances == 17:
good_jump = '3,5-6'
elif jump_chances == 18:
good_jump = '4-6'
elif jump_chances == 19:
good_jump = '2,4-7'
elif jump_chances == 20:
good_jump = '3-7'
elif jump_chances == 21:
good_jump = '2-7'
elif jump_chances == 22:
good_jump = '2-7,12'
elif jump_chances == 23:
good_jump = '2-7,11'
elif jump_chances == 24:
good_jump = '2,4-8'
elif jump_chances == 25:
good_jump = '3-8'
elif jump_chances == 26:
good_jump = '2-8'
elif jump_chances == 27:
good_jump = '2-8,12'
elif jump_chances == 28:
good_jump = '2-8,11'
elif jump_chances == 29:
good_jump = '3-9'
elif jump_chances == 30:
good_jump = '2-9'
elif jump_chances == 31:
good_jump = '2-9,12'
elif jump_chances == 32:
good_jump = '2-9,11'
elif jump_chances == 33:
good_jump = '2-10'
elif jump_chances == 34:
good_jump = '3-11'
elif jump_chances == 35:
good_jump = '2-11'
else:
good_jump = '2-12'
steal_string = f'{"`*`" if batting_card.steal_auto else ""}{good_jump}/- ({batting_card.steal_high}-{batting_card.steal_low})'
return steal_string
baserunner_string = ''
if curr_play.on_first is not None:
runcard = curr_play.on_first.card.batterscouting.battingcard
baserunner_string += f'On First: {curr_play.on_first.player.name_card_link('batting')}\nSteal: {steal_string(runcard)}, Run: {runcard.running}\n'
if curr_play.on_second is not None:
runcard = curr_play.on_second.card.batterscouting.battingcard
baserunner_string += f'On Second: {curr_play.on_second.player.name_card_link('batting')}\nSteal: {steal_string(runcard)}, Run: {runcard.running}\n'
if curr_play.on_third is not None:
runcard = curr_play.on_third.card.batterscouting.battingcard
baserunner_string += f'On Third: {curr_play.on_third.player.name_card_link('batting')}\nSteal: {steal_string(runcard)}, Run: {runcard.running}\n'
logger.info(f'gameplay_models - get_scorebug_embed - baserunner_string: {baserunner_string}')
pitchingcard = curr_play.pitcher.card.pitcherscouting.pitchingcard
battingcard = curr_play.batter.card.batterscouting.battingcard
pit_string = f'{pitchingcard.hand.upper()}HP | {curr_play.pitcher.player.name_card_link('pitching')}'
if len(baserunner_string) > 0:
logger.info(f'Adding pitcher hold to scorebug')
pitchingcard = curr_play.pitcher.card.pitcherscouting.pitchingcard
pit_string += f'\nHold: {"+" if pitchingcard.hold > 0 else ""}{pitchingcard.hold}, WP: {pitchingcard.wild_pitch}, Bk: {pitchingcard.balk}'
# battingcard = curr_play.batter.card.batterscouting.battingcard
# bat_string += f'\nBunt: {battingcard.bunting}, HnR: {battingcard.hit_and_run}'
pit_string += f'\n{get_pitching_statline(session, curr_play.pitcher)}'
embed.add_field(
name='Pitcher',
value=pit_string
)
bat_string = f'{curr_play.batter.batting_order}. {battingcard.hand.upper()} | {curr_play.batter.player.name_card_link('batting')}\n{get_batting_statline(session, curr_play.batter)}'
embed.add_field(
name='Batter',
value=bat_string
)
logger.info(f'Setting embed image to batter card')
embed.set_image(url=curr_play.batter.player.batter_card_url)
if len(baserunner_string) > 0:
logger.info(f'Adding baserunner info to embed')
embed.add_field(name=' ', value=' ', inline=False)
embed.add_field(name='Baserunners', value=baserunner_string)
c_query = session.exec(select(PositionRating).where(PositionRating.player_id == curr_play.catcher.card.player.id, PositionRating.position == 'C', PositionRating.variant == curr_play.catcher.card.variant)).all()
if len(c_query) > 0:
catcher_rating = c_query[0]
else:
log_exception(PositionNotFoundException, f'No catcher rating found for {curr_play.catcher.card.player.name}')
cat_string = f'{curr_play.catcher.player.name_card_link('batter')}\nArm: {catcher_rating.arm}, PB: {catcher_rating.pb}, OT: {catcher_rating.overthrow}'
embed.add_field(name='Catcher', value=cat_string)
if curr_play.ai_is_batting and curr_play.on_base_code > 0:
logger.info(f'Checking on baserunners for jump')
if curr_play.on_base_code in [2, 4]:
to_base = 3
elif curr_play.on_base_code in [1, 5]:
to_base = 2
else:
to_base = 4
jump_resp = curr_play.managerai.check_jump(session, this_game, to_base=to_base)
ai_note = jump_resp.ai_note
else:
logger.info(f'Checking defensive alignment')
def_align = curr_play.managerai.defense_alignment(session, this_game)
logger.info(f'def_align: {def_align}')
ai_note = def_align.ai_note
logger.info(f'gameplay_models - get_scorebug_embed - ai_note: {ai_note}')
if len(ai_note) > 0:
gm_name = this_game.home_team.gmname if this_game.ai_team == 'home' else this_game.away_team.gmname
embed.add_field(name=f'{gm_name} will...', value=ai_note, inline=False)
else:
embed.add_field(name=' ', value=' ', inline=False)
if full_length:
embed.add_field(
name=f'{this_game.away_team.abbrev} Lineup',
value=this_game.team_lineup(session, this_game.away_team)
)
embed.add_field(
name=f'{this_game.home_team.abbrev} Lineup',
value=this_game.team_lineup(session, this_game.home_team)
)
else:
embed.add_field(
name=f'{this_game.away_team.abbrev} Lineup',
value=this_game.team_lineup(session, this_game.away_team)
)
embed.add_field(
name=f'{this_game.home_team.abbrev} Lineup',
value=this_game.team_lineup(session, this_game.home_team)
)
return embed
def starting_pitcher_dropdown_view(session: Session, this_game: Game, human_team: Team, game_type: str = None):
pitchers = get_available_pitchers(session, this_game, human_team, sort='starter-desc')
logger.info(f'sorted pitchers: {pitchers}')
sp_selection = SelectStartingPitcher(
this_game=this_game,
this_team=human_team,
session=session,
league_name=this_game.game_type if game_type is None else game_type,
options=[SelectOption(label=f'{x.player.name_with_desc} (S{x.pitcherscouting.pitchingcard.starter_rating}/R{x.pitcherscouting.pitchingcard.relief_rating})', value=x.id) for x in pitchers],
placeholder='Select your starting pitcher'
)
return DropdownView(dropdown_objects=[sp_selection])
def sub_batter_dropdown_view(session: Session, this_game: Game, human_team: Team, batting_order: int):
batters = get_available_batters(session, this_game, human_team)
logger.info(f'batters: {batters}')
bat_selection = SelectBatterSub(
this_game=this_game,
this_team=human_team,
session=session,
batting_order=batting_order,
options=[SelectOption(label=f'{x.batterscouting.battingcard.hand.upper()} | {x.player.name_with_desc}', value=x.id) for x in batters],
placeholder='Select your Sub'
)
return DropdownView(dropdown_objects=[bat_selection])
async def read_lineup(session: Session, interaction: discord.Interaction, this_game: Game, lineup_team: Team, sheets_auth, lineup_num: int, league_name: str):
"""
Commits lineups and rosterlinks
"""
existing_lineups = get_game_lineups(
session=session,
this_game=this_game,
specific_team=lineup_team,
is_active=True
)
if len(existing_lineups) > 1:
await interaction.edit_original_response(
f'It looks like the {lineup_team.sname} already have a lineup. Run `/substitution` to make changes.'
)
return
await interaction.edit_original_response(content='Okay, let\'s put this lineup card together...')
session.add(this_game)
human_lineups = await get_lineups_from_sheets(session, sheets_auth, this_game, this_team=lineup_team, lineup_num=lineup_num, roster_num=this_game.away_roster_id if this_game.home_team.is_ai else this_game.home_roster_id)
await interaction.edit_original_response(content='Heard from sheets, pulling in scouting data...')
for batter in human_lineups:
session.add(batter)
session.commit()
for batter in human_lineups:
if batter.position != 'DH':
await get_position(session, batter.card, batter.position)
return this_game.initialize_play(session)
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:
inning_num = 9 if next_play.inning_num > 9 else next_play.inning_num
new_win_ex = WPA_DF.loc[f'{next_play.inning_half}_{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}')
inning_num = 9 if this_play.inning_num > 9 else this_play.inning_num
old_win_ex = WPA_DF.loc[f'{this_play.inning_half}_{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
"""
logger.info(f'Completing play {this_play.id} in game {this_play.game.id}')
nso = this_play.starting_outs + this_play.outs
runs_scored = 0
on_first, on_second, on_third = None, None, None
logger.info(f'Running bulk checks')
is_go_ahead = False
if nso >= 3:
switch_sides = True
obc = 0
nso = 0
nih = 'bot' if this_play.inning_half == '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)
logger.info(f'Calculating re24')
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)
logger.info(f'new_batter: {new_batter}')
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
logger.info(f'this_play: {this_play}')
logger.info(f'new_play: {new_play}')
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.info(f'get_lineups_from_sheets - sheets: {sheets}')
this_sheet = sheets.open_by_key(this_team.gsheet)
logger.info(f'this_sheet: {this_sheet}')
r_sheet = this_sheet.worksheet_by_title('My Rosters')
logger.info(f'r_sheet: {r_sheet}')
logger.info(f'lineup_num: {roster_num}')
if lineup_num == 1:
row_start = 9
row_end = 17
else:
row_start = 18
row_end = 26
logger.info(f'roster_num: {roster_num}')
if int(roster_num) == 1:
l_range = f'H{row_start}:I{row_end}'
elif int(roster_num) == 2:
l_range = f'J{row_start}:K{row_end}'
else:
l_range = f'L{row_start}:M{row_end}'
logger.info(f'l_range: {l_range}')
raw_cells = r_sheet.range(l_range)
logger.info(f'raw_cells: {raw_cells}')
try:
lineup_cells = [(row[0].value, int(row[1].value)) for row in raw_cells]
logger.info(f'lineup_cells: {lineup_cells}')
except ValueError as e:
logger.error(f'Could not pull roster for {this_team.abbrev}: {e}')
log_exception(GoogleSheetsException, f'Uh oh. Looks like your lineup 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.league_name)
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 get_full_roster_from_sheets(session: Session, interaction: discord.Interaction, sheets, this_game: Game, this_team: Team, roster_num: int) -> list[RosterLink]:
"""
Commits roster links
"""
logger.debug(f'get_full_roster_from_sheets - sheets: {sheets}')
this_sheet = sheets.open_by_key(this_team.gsheet)
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 roster_num == 1:
l_range = 'B3:B28'
elif roster_num == 2:
l_range = 'B29:B54'
else:
l_range = 'B55:B80'
roster_message = await interaction.channel.send(content='I\'m diving into Sheets - wish me luck.')
logger.info(f'l_range: {l_range}')
raw_cells = r_sheet.range(l_range)
logger.info(f'raw_cells: {raw_cells}')
await roster_message.edit(content='Got your roster, now to find these cards in your collection...')
try:
card_ids = [row[0].value for row in raw_cells]
logger.info(f'card_ids: {card_ids}')
except ValueError as e:
logger.error(f'Could not pull roster for {this_team.abbrev}: {e}')
log_exception(GoogleSheetsException, f'Uh oh. Looks like your roster might not be saved. I am reading blanks when I try to get the card IDs')
for x in card_ids:
this_card = await get_card_or_none(session, card_id=x)
session.add(RosterLink(
game=this_game,
card=this_card,
team=this_team
))
session.commit()
await roster_message.edit(content='Your roster is logged and scouting data is available.')
return session.exec(select(RosterLink).where(RosterLink.game == this_game, RosterLink.team == this_team)).all()
async def checks_log_interaction(session: Session, interaction: discord.Interaction, command_name: str) -> tuple[Game, Team, Play]:
"""
Commits this_play
"""
logger.info(f'log interaction checks for {interaction.user.name} in channel {interaction.channel.name}')
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()
session.refresh(this_play)
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
"""
logger.info(f'Logging a run for runner ID {runner.id} in Game {this_play.game.id}')
last_ab = get_players_last_pa(session, lineup_member=runner)
last_ab.run = 1
logger.info(f'last_ab: {last_ab}')
errors = session.exec(select(func.count(Play.id)).where(
Play.game == this_play.game, Play.inning_num == last_ab.inning_num, Play.inning_half == last_ab.inning_half, Play.error == 1, Play.complete == True
)).one()
outs = session.exec(select(func.sum(Play.outs)).where(
Play.game == this_play.game, Play.inning_num == last_ab.inning_num, Play.inning_half == last_ab.inning_half, Play.complete == True
)).one()
logger.info(f'errors: {errors} / outs: {outs}')
if outs is not None:
if errors + outs + this_play.error >= 3:
logger.info(f'unearned run')
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, only_forced: bool = False, earned_bases: int = None) -> Play:
"""
No commits
"""
logger.info(f'Advancing runners {num_bases} bases in game {this_play.game.id}')
if earned_bases is None:
earned_bases = num_bases
this_play.rbi = 0
er_from = {
3: True if earned_bases >= 1 else False,
2: True if earned_bases >= 2 else False,
1: True if earned_bases >= 3 else False
}
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
elif 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, is_earned=er_from[3])
this_play.rbi += 1 if er_from[3] else 0
if num_bases > 1:
this_play.on_second_final = 4
log_run_scored(session, this_play.on_second, this_play, is_earned=er_from[2])
this_play.rbi += 1 if er_from[2] 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, is_earned=er_from[1])
this_play.rbi += 1 if er_from[1] 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, is_earned=er_from[3])
this_play.rbi += 1 if er_from[3] 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, is_earned=er_from[2])
this_play.rbi += 1 if er_from[2] 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, is_earned=er_from[1])
this_play.rbi += 1 if er_from[1] 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:
this_play = 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
this_play = 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')
this_of = await show_outfield_cards(session, interaction, this_play)
of_rating = await get_position(session, this_of.card, this_of.position)
of_mod = 0
if this_of.position == 'LF':
of_mod = -2
elif this_of.position == 'RF':
of_mod = 2
logger.debug(f'done with of embed')
runner_lineup = this_play.on_second
runner = runner_lineup.player
max_safe = runner_lineup.card.batterscouting.battingcard.running + of_rating.arm + of_mod
min_out = 20 + of_rating.arm + of_mod
if (min_out >= 20 and max_safe >= 20) or min_out > 20:
min_out = 21
min_hold = max_safe + 1
safe_string = f'1{" - " if max_safe > 1 else ""}'
if max_safe > 1:
if max_safe <= 20:
safe_string += f'{max_safe}'
else:
safe_string += f'20'
if min_out > 20:
out_string = 'None'
else:
out_string = f'{min_out}{" - 20" if min_out < 20 else ""}'
hold_string = ''
if max_safe != min_out:
hold_string += f'{min_hold}'
if min_out - 1 > min_hold:
hold_string += f' - {min_out - 1}'
ranges_embed = this_of.team.embed
ranges_embed.title = f'Tag Play'
ranges_embed.description = f'{this_of.team.abbrev} {this_of.position} {this_of.card.player.name}\'s Throw vs {runner.name}'
ranges_embed.add_field(name=f'Runner Speed', value=runner_lineup.card.batterscouting.battingcard.running)
ranges_embed.add_field(name=f'{this_of.position} Arm', value=f'{"+" if of_rating.arm > 0 else ""}{of_rating.arm}')
ranges_embed.add_field(name=f'{this_of.position} Mod', value=f'{of_mod}')
ranges_embed.add_field(name='', value='', inline=False)
ranges_embed.add_field(name='Safe Range', value=safe_string)
ranges_embed.add_field(name='Hold Range', value=hold_string)
ranges_embed.add_field(name='Out Range', value=out_string)
await interaction.channel.send(
content=None,
embed=ranges_embed
)
if this_play.ai_is_batting:
tag_resp = this_play.managerai.tag_from_second(session, this_game)
logger.info(f'tag_resp: {tag_resp}')
tagging_from_second = tag_resp.min_safe >= max_safe
if tagging_from_second:
await interaction.channel.send(
content=f'**{runner.name}** is tagging from second!'
)
else:
await interaction.channel.send(
content=f'**{runner.name}** is holding at second.'
)
else:
tagging_from_second = await ask_confirm(
interaction,
question=f'Is {runner.name} attempting to tag up from second?',
label_type='yes',
)
if tagging_from_second:
this_roll = d_twenty_roll(this_play.pitcher.team, this_play.game)
if min_out is not None and this_roll.d_twenty >= min_out:
result = 'out'
elif this_roll.d_twenty <= max_safe:
result = 'safe'
else:
result = 'holds'
await interaction.channel.send(content=None, embeds=this_roll.embeds)
is_correct = await ask_confirm(
interaction,
question=f'Looks like {runner.name} {"is" if result != 'holds' else ""} **{result.upper()}** at {"third" if result != 'holds' else "second"}! Is that correct?',
label_type='yes',
delete_question=False
)
if not is_correct:
view = ButtonOptions(
responders=[interaction.user], timeout=60,
labels=['Safe at 3rd', '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':
result = 'safe'
elif view.value == 'Out at 3rd':
result = 'out'
else:
result = 'holds'
else:
await question.delete()
if result == 'safe':
this_play.on_second_final = 3
elif result == 'out':
num_outs += 1
this_play.on_second_final = None
this_play.outs = num_outs
elif flyball_type == 'b?':
this_play.pa, this_play.ab, this_play.outs = 1, 1, 1
this_play = advance_runners(session, this_play, 0)
if this_play.starting_outs < 2 and this_play.on_third:
logger.debug(f'calling of embed')
this_of = await show_outfield_cards(session, interaction, this_play)
of_rating = await get_position(session, this_of.card, this_of.position)
logger.debug(f'done with of embed')
runner_lineup = this_play.on_third
runner = runner_lineup.player
max_safe = runner_lineup.card.batterscouting.battingcard.running + of_rating.arm
safe_string = f'1{" - " if max_safe > 1 else ""}'
if max_safe > 1:
if max_safe <= 20:
safe_string += f'{max_safe - 1}'
else:
safe_string += f'20'
if max_safe == 20:
out_string = 'None'
catcher_string = '20'
elif max_safe > 20:
out_string = 'None'
catcher_string = 'None'
elif max_safe == 19:
out_string = 'None'
catcher_string = '19 - 20'
elif max_safe == 18:
out_string = f'20'
catcher_string = '18 - 19'
else:
out_string = f'{max_safe + 2} - 20'
catcher_string = f'{max_safe} - {max_safe + 1}'
true_max_safe = max_safe - 1
true_min_out = max_safe + 2
ranges_embed = this_play.batter.team.embed
ranges_embed.title = f'Play at the Plate'
ranges_embed.description = f'{runner.name} vs {this_of.card.player.name}\'s Throw'
ranges_embed.add_field(name=f'{this_of.position} Arm', value=f'{"+" if of_rating.arm > 0 else ""}{of_rating.arm}')
ranges_embed.add_field(name=f'Runner Speed', value=runner_lineup.card.batterscouting.battingcard.running)
ranges_embed.add_field(name="", value="", inline=False)
ranges_embed.add_field(name='Safe Range', value=safe_string)
ranges_embed.add_field(name='Catcher Check', value=catcher_string)
ranges_embed.add_field(name='Out Range', value=out_string)
await interaction.channel.send(
content=None,
embed=ranges_embed
)
if this_play.ai_is_batting:
tag_resp = this_play.managerai.tag_from_third(session, this_game)
logger.info(f'tag_resp: {tag_resp}')
tagging_from_third = tag_resp.min_safe <= max_safe
if tagging_from_third:
await interaction.channel.send(
content=f'**{runner.name}** is tagging from third!'
)
else:
await interaction.channel.send(
content=f'**{runner.name}** is holding at third.'
)
else:
tagging_from_third = await ask_confirm(
interaction,
question=f'Is {runner.name} attempting to tag up from third?',
label_type='yes',
)
if tagging_from_third:
this_roll = d_twenty_roll(this_play.batter.team, this_play.game)
if this_roll.d_twenty <= true_max_safe:
result = 'safe'
q_text = f'Looks like {runner.name} is SAFE at home!'
out_at_home = False
elif this_roll.d_twenty >= true_min_out:
result = 'out'
q_text = f'Looks like {runner.name} is OUT at home!'
out_at_home = True
else:
result = 'catcher'
q_text = f'Looks like this is a check for {this_play.catcher.player.name} to block the plate!'
await interaction.channel.send(content=None, embeds=this_roll.embeds)
is_correct = await ask_confirm(
interaction,
question=f'{q_text} Is that correct?',
label_type='yes',
delete_question=False
)
if not is_correct:
out_at_home = await ask_confirm(
interaction,
question=f'Was {runner.name} thrown out?',
label_type='yes'
)
elif result == 'catcher':
catcher_rating = await get_position(session, this_play.catcher.card, 'C')
this_roll = d_twenty_roll(this_play.catcher.team, this_play.game)
if catcher_rating.range == 1:
safe_range = 3
elif catcher_rating.range == 2:
safe_range = 7
elif catcher_rating.range == 3:
safe_range = 11
elif catcher_rating.range == 4:
safe_range = 15
elif catcher_rating.range == 5:
safe_range = 19
out_at_home = True
if this_roll.d_twenty <= safe_range:
out_at_home = False
if out_at_home:
num_outs += 1
this_play.on_third_final = None
this_play.outs = num_outs
else:
this_play.ab = 0
this_play.rbi = 1
this_play.on_third_final = 4
log_run_scored(session, this_play.on_third, this_play)
elif flyball_type == 'c':
this_play.pa, this_play.ab, this_play.outs = 1, 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 lineouts(session: Session, interaction: discord.Interaction, this_play: Play, lineout_type: Literal['one-out', 'ballpark', 'max-outs']) -> Play:
"""
Commits this_play
"""
num_outs = 1
this_play.pa, this_play.ab, this_play.outs = 1, 1, 1
this_play.bplo = 1 if lineout_type == 'ballpark' else 0
this_play = advance_runners(session, this_play, num_bases=0)
if lineout_type == 'max-outs' and this_play.on_base_code > 0 and this_play.starting_outs < 2:
logger.info(f'Lomax going in')
if this_play.on_base_code <= 3 or this_play.starting_outs == 1:
logger.info(f'Lead runner is out')
this_play.outs = 2
if this_play.on_third is not None:
this_play.on_third_final = None
elif this_play.on_second is not None:
this_play.on_second_final = None
elif this_play.on_first is not None:
this_play.on_first_final = None
else:
logger.info(f'Potential triple play')
this_roll = d_twenty_roll(this_play.pitcher.team, this_play.game)
ranges_embed = this_play.pitcher.team.embed
ranges_embed.title = f'Potential Triple Play'
ranges_embed.description = f'{this_play.pitcher.team.lname}'
ranges_embed.add_field(name=f'Double Play Range', value='1 - 13')
ranges_embed.add_field(name=f'Triple Play Range', value='14 - 20')
await interaction.edit_original_response(
content=None,
embeds=[ranges_embed, *this_roll.embeds]
)
if this_roll.d_twenty > 13:
logger.info(f'Roll of {this_roll.d_twenty} is a triple play!')
num_outs = 3
else:
logger.info(f'Roll of {this_roll.d_twenty} is a double play!')
num_outs = 2
is_correct = await ask_confirm(
interaction,
question=f'Looks like this is a {"triple" if num_outs == 3 else "double"} play! Is that correct?'
)
if not is_correct:
logger.warning(f'{interaction.user.name} marked this result incorrect')
num_outs = 2 if num_outs == 3 else 3
if num_outs == 2:
logger.info(f'Lead baserunner is out')
this_play.outs = 2
out_marked = False
if this_play.on_third is not None:
this_play.on_third_final = None
out_marked = True
elif this_play.on_second and not out_marked:
this_play.on_second_final = None
out_marked = True
elif this_play.on_first and not out_marked:
this_play.on_first_final = None
out_marked = True
else:
logger.info(f'Two baserunners are out')
this_play.outs = 3
outs_marked = 1
if this_play.on_third is not None:
this_play.on_third_final = None
outs_marked += 1
elif this_play.on_second is not None:
this_play.on_second_final = None
outs_marked += 1
elif this_play.on_first is not None and outs_marked < 3:
this_play.on_first_final = None
outs_marked += 1
session.add(this_play)
session.commit()
session.refresh(this_play)
return this_play
async def frame_checks(session: Session, interaction: discord.Interaction, this_play: Play):
"""
Commits this_play
"""
this_roll = frame_plate_check(this_play.batter.team, this_play.game)
logger.info(f'this_roll: {this_roll}')
await interaction.edit_original_response(
content=None,
embeds=this_roll.embeds
)
if this_roll.is_walk:
this_play = await walks(session, interaction, this_play, 'unintentional')
else:
this_play = await strikeouts(session, interaction, this_play)
session.add(this_play)
session.commit()
await asyncio.sleep(1.5)
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}')
this_roll = d_twenty_roll(this_play.batter.team, this_play.game)
block_roll = d_twenty_roll(this_play.catcher.team, this_play.game)
def_team = this_play.pitcher.team
runner_bc = get_batter_card(this_lineup=lead_runner)
of_rating = await get_position(session, this_card=outfielder.card, position=outfielder.position)
c_rating = await get_position(session, this_play.catcher.card, position='C')
runner_embed = this_play.batter.team.embed
safe_range = None
def_alignment = this_play.managerai.defense_alignment(session, this_play.game)
lead_bc = get_batter_card(this_lineup=lead_runner)
logger.info(f'lead runner batting card: {lead_bc}')
lead_safe_range = lead_bc.running + of_rating.arm
logger.info(f'lead_safe_range: {lead_safe_range}')
# Build lead runner embed
lead_runner_embed = copy.deepcopy(runner_embed)
lead_runner_embed.title = f'{lead_runner.player.name} To {"Home" if lead_base == 4 else "Third"}'
lead_runner_embed.description = f'{outfielder.team.abbrev} {outfielder.position} {outfielder.player.name}\'s Throw'
lead_runner_embed.add_field(name=f'Runner Speed', value=lead_runner.card.batterscouting.battingcard.running)
lead_runner_embed.add_field(name=f'{outfielder.position} Arm', value=f'{"+" if of_rating.arm > 0 else ""}{of_rating.arm}')
if this_play.starting_outs == 2:
logger.info(f'Adding 2 for 2 outs')
lead_safe_range += 2
lead_runner_embed.add_field(name='2-Out Mod', value=f'+2')
if lead_base == 3 and outfielder.position != 'CF':
of_mod = -2 if outfielder.position == 'LF' else 2
logger.info(f'{outfielder.position} to 3B mod: {of_mod}')
lead_safe_range += of_mod
lead_runner_embed.add_field(name=f'{outfielder.position} Mod', value=f'{"+" if of_mod > 0 else ""}{of_mod}')
logger.info(f'lead_runner_embed: {lead_runner_embed}')
# Build trail runner embed
trail_runner_embed = copy.deepcopy(runner_embed)
trail_bc = get_batter_card(this_lineup=trail_runner)
logger.info(f'trail runner batting card: {trail_bc}')
trail_runner_embed.title = f'{trail_runner.player.name} To {"Third" if trail_base == 3 else "Second"}'
trail_runner_embed.description = f'{outfielder.team.abbrev} {outfielder.position} {outfielder.player.name}\'s Throw'
trail_runner_embed.add_field(name=f'Runner Speed', value=trail_bc.running)
trail_runner_embed.add_field(name=f'{outfielder.position} Arm', value=f'{"+" if of_rating.arm > 0 else ""}{of_rating.arm}')
trail_safe_range = trail_bc.running - 5 + of_rating.arm
logger.info(f'trail_safe_range: {trail_safe_range}')
if trail_base == 3 and outfielder.position != 'CF':
of_mod = -2 if outfielder.position == 'LF' else 2
logger.info(f'{outfielder.position} to 3B mod: {of_mod}')
trail_safe_range += of_mod
trail_runner_embed.add_field(name=f'{outfielder.position} Mod', value=f'{"+" if of_mod > 0 else ""}{of_mod}', inline=False)
trail_runner_embed.add_field(name='Trail Runner', value='-5')
def at_home_strings(safe_range: int):
safe_string = f'1{" - " if safe_range > 1 else ""}'
if safe_range > 1:
if safe_range <= 20:
safe_string += f'{safe_range - 1}'
else:
safe_string += f'20'
if safe_range == 20:
out_string = 'None'
catcher_string = '20'
elif safe_range > 20:
out_string = 'None'
catcher_string = 'None'
elif safe_range == 19:
out_string = 'None'
catcher_string = '19 - 20'
elif safe_range == 18:
out_string = f'20'
catcher_string = '18 - 19'
else:
out_string = f'{safe_range + 2} - 20'
catcher_string = f'{safe_range} - {safe_range + 1}'
logger.info(f'safe: {safe_string} / catcher: {catcher_string} / out: {out_string}')
return {'safe': safe_string, 'catcher': catcher_string, 'out': out_string}
def at_third_strings(safe_range: int):
safe_string = f'1{" - " if safe_range > 1 else ""}'
if safe_range > 1:
if safe_range <= 20:
safe_string += f'{safe_range}'
else:
safe_string += f'20'
if safe_range > 19:
out_string = '20'
else:
out_string = f'{safe_range + 1} - 20'
logger.info(f'safe: {safe_string} / out: {out_string}')
return {'safe': safe_string, 'out': out_string}
async def out_at_home(safe_range: int):
if this_roll.d_twenty in [safe_range, safe_range + 1]:
logger.info(f'Roll of {this_roll.d_twenty} is a catcher check with safe range of {safe_range}')
is_block_plate = await ask_confirm(
interaction,
question=f'Looks like **{this_play.catcher.player.name}** has a chance to block the plate! Is that correct?',
label_type='yes',
delete_question=False
)
if is_block_plate:
logger.info(f'Looks like a block the plate check')
await interaction.channel.send(content=None, embeds=block_roll.embeds)
if block_roll.d_twenty > RANGE_CHECKS[c_rating]:
logger.info(f'Roll of {this_roll.d_twenty} is OUT {AT_BASE[4]}')
runner_thrown_out = True
q_text = f'Looks like **{lead_runner.player.name}** is OUT {AT_BASE[4]}!'
else:
logger.info(f'Roll of {this_roll.d_twenty} is SAFE {AT_BASE[4]}')
runner_thrown_out = False
q_text = f'Looks like **{lead_runner.player.name}** is SAFE {AT_BASE[4]}!'
else:
runner_thrown_out = await ask_confirm(
interaction=interaction,
question=f'Was **{lead_runner.player.name}** thrown out {AT_BASE[4]}?',
label_type='yes',
)
else:
logger.info(f'Roll of {this_roll.d_twenty} has a clear result with safe range of {safe_range}')
if this_roll.d_twenty > safe_range:
logger.info(f'Roll of {this_roll.d_twenty} is OUT {AT_BASE[4]}')
runner_thrown_out = True
q_text = f'Looks like **{lead_runner.player.name}** is OUT {AT_BASE[4]}!'
else:
logger.info(f'Roll of {this_roll.d_twenty} is SAFE {AT_BASE[4]}')
runner_thrown_out = False
q_text = f'Looks like **{lead_runner.player.name}** is SAFE {AT_BASE[4]}!'
is_correct = await ask_confirm(
interaction,
question=f'{q_text} Is that correct?',
label_type='yes',
delete_question=False
)
if not is_correct:
logger.warning(f'{interaction.user.name} says call is incorrect; runner is {"not " if runner_thrown_out else ""}thrown out')
runner_thrown_out = not runner_thrown_out
return runner_thrown_out
async def out_at_base(safe_range: int, this_runner: Lineup, this_base: int):
if this_roll.d_twenty > safe_range:
logger.info(f'Roll of {this_roll.d_twenty} is OUT {AT_BASE[this_base]}')
runner_thrown_out = True
q_text = f'Looks like **{this_runner.player.name}** is OUT {AT_BASE[this_base]}!'
else:
logger.info(f'Roll of {this_roll.d_twenty} is SAFE {AT_BASE[this_base]}')
runner_thrown_out = False
q_text = f'Looks like **{this_runner.player.name}** is SAFE {AT_BASE[this_base]}!'
is_correct = await ask_confirm(
interaction,
question=f'{q_text} Is that correct?',
label_type='yes',
delete_question=False
)
if not is_correct:
logger.warning(f'{interaction.user.name} says call is incorrect; runner is {"not " if runner_thrown_out else ""}thrown out')
runner_thrown_out = not runner_thrown_out
return runner_thrown_out
# Either there is no AI team or the AI is pitching
if not this_game.ai_team or not this_play.ai_is_batting:
# Build lead runner embed
# Check for lead runner hold
if (lead_runner == this_play.on_second and def_alignment.hold_second) or (lead_runner == this_play.on_first and def_alignment.hold_first):
lead_safe_range -= 1
logger.info(f'Lead runner was held, -1 to safe range: {lead_safe_range}')
lead_runner_embed.add_field(name='Runner Held', value='-1')
else:
logger.info(f'Lead runner was not held, +1 to safe range: {lead_safe_range}')
lead_safe_range += 1
lead_runner_embed.add_field(name='Runner Not Held', value='+1')
lead_runner_embed.add_field(name='', value='', inline=False)
if lead_base == 4:
logger.info(f'lead base is 4, building strings')
lead_strings = at_home_strings(lead_safe_range)
lead_runner_embed.add_field(name='Safe Range', value=lead_strings['safe'])
lead_runner_embed.add_field(name='Catcher Check', value=lead_strings['catcher'])
lead_runner_embed.add_field(name='Out Range', value=lead_strings['out'])
else:
logger.info(f'lead base is 3, building strings')
lead_strings = at_third_strings(lead_safe_range)
lead_runner_embed.add_field(name='Safe Range', value=lead_strings['safe'])
lead_runner_embed.add_field(name='Out Range', value=lead_strings['out'])
# Build trail runner embed
if (trail_runner == this_play.on_first and def_alignment.hold_first):
trail_safe_range -= 1
logger.info(f'Trail runner was held, -1 to safe range: {trail_safe_range}')
trail_runner_embed.add_field(name='Runner Held', value='-1')
elif (trail_runner == this_play.on_first and not def_alignment.hold_first):
trail_safe_range += 1
logger.info(f'Trail runner was not held, +1 to safe range: {trail_safe_range}')
trail_runner_embed.add_field(name='Runner Not Held', value='+1')
else:
logger.info('Trail runner was not from first base, no hold modifier')
trail_runner_embed.add_field(name='', value='', inline=False)
logger.info(f'Building strings for trail runner')
safe_string = f'1{" - " if trail_safe_range > 1 else ""}'
if trail_safe_range > 1:
if trail_safe_range < 20:
safe_string += f'{trail_safe_range}'
else:
logger.info(f'capping safe range at 19')
trail_safe_range = 19
safe_string += f'19'
out_string = f'{trail_safe_range + 1} - 20'
logger.info(f'safe: {safe_string} / out: {out_string}')
trail_runner_embed.add_field(name='Safe Range', value=safe_string)
trail_runner_embed.add_field(name='Out Range', value=out_string)
await interaction.channel.send(embeds=[lead_runner_embed, trail_runner_embed])
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:
await interaction.channel.send(content=f'**{outfielder.player.name}** is throwing {TO_BASE[lead_base]}!')
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:
throw_lead = False
if this_game.ai_team:
if throw_resp.at_trail_runner and trail_safe_range <= throw_resp.trail_max_safe and trail_safe_range <= throw_resp.trail_max_safe_delta - lead_safe_range:
logger.info(f'defense throwing at trail runner {AT_BASE[trail_base]}')
await interaction.channel.send(f'**{outfielder.player.name}** will throw {TO_BASE[trail_base]}!')
throw_lead = False
else:
logger.info(f'defense throwing at lead runner {AT_BASE[lead_base]}')
await interaction.channel.send(f'**{outfielder.player.name}** will throw {TO_BASE[lead_base]}!')
throw_lead = True
else:
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'
question = await interaction.channel.send(
f'Is the throw going {TO_BASE[lead_base]} or {TO_BASE[trail_base]}?',
view=view
)
throw_lead = await view.wait()
# Throw is going to lead runner
if throw_lead:
logger.info(f'Throw is going to lead base')
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
await interaction.channel.send(content=None, embeds=this_roll.embeds)
runner_thrown_out = await out_at_base(trail_safe_range, trail_runner, trail_base)
# Trail runner is thrown out
if runner_thrown_out:
logger.info(f'logging one one additional out for trail runner')
# 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
logger.info(f'advancing lead runner')
if this_play.on_second == lead_runner:
logger.info(f'run scored from second')
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:
logger.info(f'run scored from first')
this_play.rbi += 1
log_run_scored(session, lead_runner, this_play)
if trail_runner != this_play.batter:
logger.info(f'Trail runner is not batter, advancing batter')
this_play.batter_final += 1
return this_play
# Ball is going to lead base, ask if safe
await interaction.channel.send(content=None, embeds=this_roll.embeds)
runner_thrown_out = await out_at_home(lead_safe_range) if lead_base == 4 else await out_at_base(lead_safe_range, lead_runner, lead_base)
# 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)
lead_runner_held = await ask_confirm(
interaction=interaction,
question=f'Was **{lead_runner.player.name}** held at {"second" if lead_runner == this_play.on_second else "first"} before the pitch?',
label_type='yes'
)
if lead_runner_held:
lead_safe_range -= 1
lead_runner_embed.add_field(name='Runner Held', value='-1')
logger.info(f'runner was held, -1 to lead safe range: {lead_safe_range}')
else:
lead_safe_range += 1
lead_runner_embed.add_field(name='Runner Not Held', value='+1')
logger.info(f'runner was not held, +1 to lead safe range: {lead_safe_range}')
if lead_safe_range < run_resp.min_safe:
logger.info(f'AI is not advancing with lead runner')
return this_play
logger.info(f'Building embeds')
lead_runner_embed.add_field(name='', value='', inline=False)
if lead_base == 4:
logger.info(f'lead base is 4, building strings')
lead_strings = at_home_strings(lead_safe_range)
lead_runner_embed.add_field(name='Safe Range', value=lead_strings['safe'])
lead_runner_embed.add_field(name='Catcher Check', value=lead_strings['catcher'])
lead_runner_embed.add_field(name='Out Range', value=lead_strings['out'])
else:
logger.info(f'lead base is 3, building strings')
lead_strings = at_third_strings(lead_safe_range)
lead_runner_embed.add_field(name='Safe Range', value=lead_strings['safe'])
lead_runner_embed.add_field(name='Out Range', value=lead_strings['out'])
if trail_runner == this_play.on_first:
trail_runner_held = await ask_confirm(
interaction=interaction,
question=f'Was **{trail_runner.player.name}** held at first before the pitch?',
label_type='yes'
)
logger.info(f'Trail runner held: {trail_runner_held}')
if trail_runner_held:
trail_runner_embed.add_field(name='Runner Held', value=f'-1')
trail_safe_range -= 1
logger.info(f'Trail runner held, -1 to safe range: {trail_safe_range}')
else:
trail_runner_embed.add_field(name='Runner Not Held', value='+1')
trail_safe_range += 1
logger.info(f'Trail runner not held, +1 to safe range: {trail_safe_range}')
trail_strings = at_third_strings(trail_safe_range)
trail_runner_embed.add_field(name='', value='', inline=False)
trail_runner_embed.add_field(name='Safe Range', value=trail_strings['safe'])
trail_runner_embed.add_field(name='Out Range', value=trail_strings['out'])
await interaction.channel.send(embeds=[lead_runner_embed, trail_runner_embed])
is_defense_throwing = await ask_confirm(
interaction=interaction,
question=f'{lead_runner.player.name} is advancing {TO_BASE[lead_base]} with a safe range of **1->{lead_safe_range if lead_base == 3 else {lead_safe_range - 1}}**! Is the defense throwing?',
label_type='yes'
)
# Human defense is not throwing for lead runner
if not is_defense_throwing:
logger.info(f'Defense is not throwing for lead runner')
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:
if this_play.double:
this_play.rbi += 1
this_play.on_first_final = 4
log_run_scored(session, lead_runner, this_play)
else:
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:
runner_thrown_out = await out_at_base(trail_safe_range, trail_runner, trail_base)
if runner_thrown_out:
logger.info(f'Runner was thrown out')
# Log out on play
this_play.outs += 1
# Remove trail runner
logger.info(f'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
logger.info(f'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)
if trail_runner != this_play.batter:
logger.info(f'Trail runner is not batter, advancing batter')
this_play.batter_final += 1
return this_play
else:
await interaction.channel.send(content=f'**{trail_runner.player.name}** is NOT trailing to {TO_BASE[trail_base]}.')
# Ball is going to lead base, ask if safe
logger.info(f'Throw is going to lead base')
await interaction.channel.send(content=None, embeds=this_roll.embeds)
runner_thrown_out = await out_at_home(lead_safe_range) if lead_base == 4 else await out_at_base(lead_safe_range, trail_runner, trail_base)
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
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')
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 == '**':
this_play = advance_runners(session, this_play, num_bases=2)
elif single_type in ['*', 'ballpark']:
this_play = advance_runners(session, this_play, num_bases=1)
this_play.bp1b = 1 if single_type == 'ballpark' else 0
elif single_type == 'uncapped':
this_play = 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
log_run_scored(session, this_play.batter, this_play)
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.batter_final = 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
this_play.outs = 1
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)
elif bunt_type == 'bad':
this_play = advance_runners(session, this_play, num_bases=1)
this_play.batter_final = 1
if this_play.on_third is not None:
this_play.on_third_final = None
elif this_play.on_second is not None:
this_play.on_second_final = None
elif this_play.on_first is not None:
this_play.on_first_final = None
elif bunt_type == 'double-play':
this_play = advance_runners(session, this_play, num_bases=0)
this_play.outs = 2 if this_play.starting_outs < 2 else 1
if this_play.on_third is not None:
this_play.on_third_final = None
elif this_play.on_second is not None:
this_play.on_second_final = None
elif this_play.on_first is not None:
this_play.on_first_final = None
elif bunt_type == 'defense':
if this_play.on_third is not None:
runner = this_play.on_third
lead_base = 4
elif this_play.on_second is not None:
runner = this_play.on_second
lead_base = 3
elif this_play.on_first is not None:
runner = this_play.on_first
lead_base = 2
take_sure_out = await ask_confirm(
interaction=interaction,
question=f'Will you take the sure out at first or throw {TO_BASE[lead_base]} for **{runner.player.name}**?',
custom_confirm_label='Out at first',
custom_cancel_label=f'Throw {TO_BASE[lead_base]}'
)
if take_sure_out:
this_play.ab = 0
this_play.sac = 1
this_play = advance_runners(session, this_play, num_bases=1)
else:
view = ButtonOptions(
responders=[interaction.user],
timeout=30,
labels=['Pitcher', 'Catcher', 'First Base', 'Third Base', None]
)
question = await interaction.channel.send(
content='Which defender is fielding the bunt? This is determined by the first d6 in your AB roll.',
view=view
)
await view.wait()
if view.value:
await question.delete()
if view.value == 'Pitcher':
defender = this_play.pitcher
elif view.value == 'Catcher':
defender = this_play.catcher
elif view.value == 'First Base':
defender = get_one_lineup(session, this_play.game, this_play.batter.team, position='1B')
elif view.value == 'Third Base':
defender = get_one_lineup(session, this_play.game, this_play.batter.team, position='3B')
else:
log_exception(NoPlayerResponseException, f'I do not know which defender fielded that ball.')
else:
await question.edit(content='You keep thinking on it and try again.', view=None)
log_exception(NoPlayerResponseException, f'{interaction.user.name} did not know who was fielding the bunt.')
def_pos = await get_position(session, defender.card, defender.position)
lead_runner_out = await ask_confirm(
interaction=interaction,
question=f'{runner.player.name}\'s safe range is **1->{runner.card.batterscouting.battingcard.running - 4 + def_pos.range}**. Is the runner out {AT_BASE[lead_base]}?',
custom_confirm_label=f'Out {AT_BASE[lead_base]}',
custom_cancel_label=f'Safe {AT_BASE[lead_base]}'
)
if lead_runner_out:
this_play = advance_runners(session, this_play, 1)
this_play.batter_final = 1
if this_play.on_third is not None:
this_play.on_third_final = None
elif this_play.on_second is not None:
this_play.on_second_final = None
elif this_play.on_first is not None:
this_play.on_first_final = None
else:
this_play.outs = 0
this_play.batter_final = 1
this_play = advance_runners(session, this_play, 1)
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
async def chaos(session: Session, interaction: discord.Interaction, this_play: Play, chaos_type: Literal['wild-pitch', 'passed-ball', 'balk', 'pickoff']):
"""
Commits this_play
"""
this_play.pa, this_play.ab = 0, 0
if chaos_type == 'wild-pitch':
this_play = advance_runners(session, this_play, 1)
this_play.rbi = 0
this_play.wild_pitch = 1
elif chaos_type == 'passed-ball':
this_play = advance_runners(session, this_play, 1)
this_play.rbi = 0
this_play.passed_ball = 1
elif chaos_type == 'balk':
this_play = advance_runners(session, this_play, 1)
this_play.rbi = 0
this_play.balk = 1
elif chaos_type == 'pickoff':
this_play = advance_runners(session, this_play, 0)
this_play.pick_off = 1
this_play.outs = 1
if this_play.on_third:
this_play.on_third_final = None
elif this_play.on_second:
this_play.on_second_final = None
elif this_play.on_first:
this_play.on_first_final = None
session.add(this_play)
session.commit()
session.refresh(this_play)
return this_play
async def steals(session: Session, interaction: discord.Interaction, this_play: Play, steal_type: Literal['stolen-base', 'caught-stealing', 'steal-plus-overthrow'], to_base: Literal[2, 3, 4]) -> Play:
this_play = advance_runners(session, this_play, 0)
this_play.pa = 0
if steal_type in ['stolen-base', 'steal-plus-overthrow']:
this_play.sb = 1
this_play.error = 1 if steal_type == 'steal-plus-overthrow' else 0
if to_base == 4 and this_play.on_third:
this_play.runner = this_play.on_third
this_play.on_third_final = 4
log_run_scored(session, this_play.on_third, this_play)
if this_play.on_second:
this_play.on_second_final = 3
if steal_type == 'steal-plus-overthrow':
this_play.on_second_final = 4
log_run_scored(session, this_play.on_second, this_play, is_earned=False)
if this_play.on_first:
this_play.on_first_final = 2 if steal_type == 'stolen-base' else 3
elif to_base == 3 and this_play.on_second:
this_play.runner = this_play.on_second
this_play.on_second_final = 3
if this_play.on_first:
this_play.on_first_final = 2
if steal_type == 'steal-plus-overthrow':
this_play.on_second_final = 4
log_run_scored(session, this_play.on_second, this_play, is_earned=False)
if this_play.on_first:
this_play.on_first_final = 3
else:
this_play.runner = this_play.on_first
this_play.on_first_final = 2 if steal_type == 'stolen-base' else 3
elif steal_type == 'caught-stealing':
this_play.outs = 1
if to_base == 4 and this_play.on_third:
this_play.runner = this_play.on_third
this_play.on_third_final = None
if this_play.on_second:
this_play.on_second_final = 3
if this_play.on_first:
this_play.on_first_final = 2
elif to_base == 3 and this_play.on_second:
this_play.runner = this_play.on_second
this_play.on_second_final = None
if this_play.on_first:
this_play.on_first_final = 2
else:
this_play.runner = this_play.on_first
this_play.on_first_final = None
session.add(this_play)
session.commit()
session.refresh(this_play)
return this_play
async def xchecks(session: Session, interaction: discord.Interaction, this_play: Play, position: str, debug: bool = False) -> Play:
defense_team = this_play.pitcher.team
this_defender = get_one_lineup(
session,
this_play.game,
this_team=defense_team,
position=position
)
this_play.defender = this_defender
this_play.check_pos = position
def_alignment = this_play.managerai.defense_alignment(session, this_play.game)
defender_is_in = def_alignment.defender_in(position)
playing_in = False
defender_embed = defense_team.embed
defender_embed.title = f'{defense_team.sname} {position} Check'
defender_embed.description = f'{this_defender.player.name}'
defender_embed.set_image(url=this_defender.player.image)
logger.info(f'defender_embed: {defender_embed}')
# if not debug:
# await interaction.edit_original_response(content=None, embeds=embeds)
this_rating = await get_position(session, this_defender.card, position)
logger.info(f'position rating: {this_rating}')
if this_play.on_third is not None:
if not this_play.ai_is_batting and defender_is_in:
playing_in = True
elif this_play.ai_is_batting:
playing_in = await ask_confirm(
interaction,
question=f'Was {this_defender.card.player.name} playing in?',
label_type='yes'
)
if playing_in:
this_rating.range = min(this_rating.range + 1, 5)
this_roll = sa_fielding_roll(defense_team, this_play, position, this_rating)
logger.info(f'this_roll: {this_roll}')
if not debug:
question = f'Looks like this is a **{this_roll.hit_result}**'
if this_roll.is_chaos:
question += ' **rare play**'
elif this_roll.error_result is not None:
question += f' plus {this_roll.error_result}-base error'
question += f'. Is that correct?'
await interaction.edit_original_response(
content=None,
embeds=[defender_embed, *this_roll.embeds]
)
is_correct = await ask_confirm(
interaction,
question,
label_type='yes',
timeout=30,
)
else:
is_correct = True
hit_result = this_roll.hit_result
error_result = this_roll.error_result
is_rare_play = this_roll.is_chaos
logger.info(f'X-Check in Game #{this_play.game_id} at {this_play.check_pos} for {this_play.defender.card.player.name_with_desc} of the {this_play.pitcher.team.sname} / hit_result: {hit_result} / error_result: {error_result} / is_correct: {is_correct}')
if not is_correct:
# Full questionnaire
pass
if hit_result == 'SPD' and not is_rare_play:
is_out = ask_confirm(
interaction,
f'Is {this_play.batter.player.name} thrown out at first?',
custom_confirm_label='Out at first',
custom_cancel_label='Safe at first'
)
if is_out:
hit_result = 'G3'
else:
this_play = await singles(session, interaction, this_play, '*')
if '#' in hit_result:
logger.info(f'Checking if the # result becomes a hit')
if this_play.ai_is_batting:
if (position in ['1B', '3B', 'P', 'C'] and def_alignment.corners_in) or (position in ['1B, ''2B', '3B', 'SS', 'P', 'C'] and def_alignment.infield_in):
hit_result = 'SI2'
elif this_play.on_base_code > 0:
is_holding = False
if not playing_in:
if position == '1B' and this_play.on_first is not None:
is_holding = await ask_confirm(
interaction,
question=f'Was {this_play.on_second.card.player.name} held at first base?',
label_type='yes'
)
elif position == '3B' and this_play.on_second is not None:
is_holding = await ask_confirm(
interaction,
question=f'Was {this_play.on_second.card.player.name} held at second base?',
label_type='yes'
)
elif position == '2B' and (this_play.on_first is not None or this_play.on_second is not None) and (this_play.batter.card.batterscouting.battingcard.hand == 'R' or (this_play.batter.card.batterscouting.battingcard.hand == 'S' and this_play.pitcher.card.pitcherscouting.pitchingcard.hand == 'L')):
if this_play.on_second is not None:
is_holding = await ask_confirm(
interaction,
question=f'Was {this_play.on_second.card.player.name} held at second base?',
label_type='yes'
)
elif this_play.on_first is not None:
is_holding = await ask_confirm(
interaction,
question=f'Was {this_play.on_first.card.player.name} held at first base?',
label_type='yes'
)
elif position == 'SS' and (this_play.on_first is not None or this_play.on_second is not None) and (this_play.batter.card.batterscouting.battingcard.hand == 'L' or (this_play.batter.card.batterscouting.battingcard.hand == 'S' and this_play.pitcher.card.pitcherscouting.pitchingcard.hand == 'R')):
if this_play.on_second is not None:
is_holding = await ask_confirm(
interaction,
question=f'Was {this_play.on_second.card.player.name} held at second base?',
label_type='yes'
)
elif this_play.on_first is not None:
is_holding = await ask_confirm(
interaction,
question=f'Was {this_play.on_first.card.player.name} held at first base?',
label_type='yes'
)
if is_holding or playing_in:
hit_result = 'SI2'
if is_rare_play:
logger.info(f'Is rare play')
if hit_result == 'SI1':
this_play = await singles(session, interaction, this_play, '*')
if this_play.on_first is None:
this_play.error = 1
this_play.batter_final = 2
elif hit_result == 'SI2':
this_play = await singles(session, interaction, this_play, '**')
this_play.batter_final = None
this_play.outs = 1
elif 'DO' in hit_result:
this_play = await doubles(session, interaction, this_play, '***')
this_play.batter_final = None
this_play.outs = 1
elif hit_result == 'TR':
this_play = await triples(session, interaction, this_play)
this_play.batter_final = 4
this_play.run = 1
this_play.error = 1
elif hit_result == 'PO':
this_play = advance_runners(session, this_play, 1, earned_bases=0)
this_play.ab, this_play.error, this_play.batter_final = 1, 1, 1
elif hit_result == 'FO':
this_play = advance_runners(session, this_play, 1, is_error=True, only_forced=True)
this_play.ab, this_play.error, this_play.batter_final = 1, 1, 1
elif hit_result == 'G1':
if this_play.on_first is not None and this_play.starting_outs < 2:
this_play = await gb_letter(session, interaction, this_play, 'B', position=this_play.check_pos, defender_is_in=playing_in)
else:
this_play = await gb_letter(session, interaction, this_play, 'A', position=this_play.check_pos, defender_is_in=playing_in)
elif hit_result == 'G2':
if this_play.on_base_code > 0:
this_play = await gb_letter(session, interaction, this_play, 'C', position=this_play.check_pos, defender_is_in=playing_in)
else:
this_play = await gb_letter(session, interaction, this_play, 'B', position=this_play.check_pos, defender_is_in=playing_in)
elif hit_result == 'G3':
if this_play.on_base_code > 0:
this_play = await singles(session, interaction, this_play, '*')
else:
this_play = await gb_letter(session, interaction, this_play, 'C', position=this_play.check_pos, defender_is_in=playing_in)
elif hit_result == 'SPD':
this_play = singles(session, interaction, this_play, '*')
elif hit_result == 'F1':
this_play.outs = 1
this_play.ab = 1 if this_play.on_third is None else 0
if this_play.on_base_code > 0 and this_play.starting_outs < 2:
this_play = advance_runners(session, this_play, 1)
if this_play.on_second is not None:
this_play.on_second_final = 4
log_run_scored(session, this_play.on_second, this_play, is_earned=False)
elif this_play.on_first is not None:
this_play.on_first_final = 3
elif hit_result == 'F2':
this_play.outs = 1
this_play.ab = 1 if this_play.on_third is None else 0
if this_play.on_base_code > 0 and this_play.starting_outs < 2:
this_play.on_third_final = None
this_play.outs = 2
else:
this_play.outs = 1
this_play.ab = 1
if this_play.on_third:
this_play.outs = 2
this_play.on_third_final = None
elif this_play.on_second:
this_play.outs = 2
this_play.on_second_final = None
elif this_play.on_first:
this_play.outs = 2
this_play.on_first_final = None
elif hit_result not in ['SI1', 'SI2', 'DO2', 'DO3', 'TR'] and error_result is None:
logger.info(f'Not a hit, not an error')
if this_play.on_base_code == 0:
this_play = await gb_result(session, interaction, this_play, 1)
else:
to_mif = position in ['2B', 'SS']
to_right_side = position in ['1B', '2B']
if 'G3' in hit_result:
if this_play.on_base_code == 2 and not playing_in:
this_play = await gb_result(session, interaction, this_play, 12)
elif playing_in and this_play.on_base_code == 5:
this_play = await gb_result(session, interaction, this_play, 7, to_mif, to_right_side)
elif playing_in and this_play.on_base_code in [3, 6]:
this_play = await gb_decide(session, interaction=interaction, this_play=this_play)
elif playing_in and this_play.on_base_code == 7:
this_play = await gb_result(session, interaction, this_play, 11)
else:
this_play = await gb_result(session, interaction, this_play, 3)
elif 'G2' in hit_result:
if this_play.on_base_code == 7 and playing_in:
this_play = await gb_result(session, interaction, this_play, 11)
elif not playing_in and this_play.on_base_code in [3, 6]:
this_play = await gb_result(session, interaction, this_play, 5, to_mif=to_mif)
elif playing_in and this_play.on_base_code in [3, 5, 6]:
this_play = await gb_result(session, interaction, this_play, 1)
elif this_play.on_base_code == 2:
this_play = await gb_result(session, interaction, this_play, 12)
else:
this_play = await gb_result(session, interaction, this_play, 4)
elif 'G1' in hit_result:
if this_play.on_base_code == 7 and playing_in:
this_play = await gb_result(session, interaction, this_play, 10)
elif not playing_in and this_play.on_base_code == 4:
this_play = await gb_result(session, interaction, this_play, 13)
elif not playing_in and this_play.on_base_code in [3, 6]:
this_play = await gb_result(session, interaction, this_play, 3)
elif playing_in and this_play.on_base_code in [3, 5, 6]:
this_play = await gb_result(session, interaction, this_play, 1)
elif this_play.on_base_code == 2:
this_play = await gb_result(session, interaction, this_play, 12)
else:
this_play = await gb_result(session, interaction, this_play, 2)
elif 'F1' in hit_result:
this_play = await flyballs(session, interaction, this_play, 'a')
elif 'F2' in hit_result:
this_play = await flyballs(session, interaction, this_play, 'b')
elif 'F3' in hit_result:
this_play = await flyballs(session, interaction, this_play, 'c')
# FO and PO
else:
this_play.ab, this_play.outs = 1, 1
this_play = advance_runners(session, this_play, 0)
elif hit_result not in ['SI1', 'SI2', 'DO2', 'DO3', 'TR'] and error_result is not None:
logger.info(f'Not a hit, {error_result}-base error')
this_play = advance_runners(session, this_play, error_result, earned_bases=0)
this_play.ab, this_play.error, this_play.batter_final = 1, 1, error_result
else:
logger.info(f'Hit result: {hit_result}, Error: {error_result}')
if hit_result == 'SI1' and error_result is None:
this_play = await singles(session, interaction, this_play, '*')
elif hit_result == 'SI1':
this_play.ab, this_play.hit, this_play.error, this_play.batter_final = 1, 1, 1, 2
this_play = advance_runners(session, this_play, num_bases=error_result + 1, earned_bases=1)
elif hit_result == 'SI2' and error_result is None:
this_play = await singles(session, interaction, this_play, '**')
elif hit_result == 'SI2':
this_play.ab, this_play.hit, this_play.error = 1, 1, 1
if error_result > 1:
num_bases = 3
this_play.batter_final = 3
else:
num_bases = 2
this_play.batter_final = 2
this_play = advance_runners(session, this_play, num_bases=num_bases, earned_bases=2)
elif hit_result == 'DO2' and error_result is None:
this_play = await doubles(session, interaction, this_play, '**')
elif hit_result == 'DO2':
this_play.ab, this_play.hit, this_play.error, this_play.double = 1, 1, 1, 1
num_bases = 3
if error_result == 3:
this_play.batter_final = 4
else:
this_play.batter_final = 3
this_play = advance_runners(session, this_play, num_bases=num_bases, earned_bases=2)
elif hit_result == 'DO3' and error_result is None:
this_play = await doubles(session, interaction, this_play, '***')
elif hit_result == 'DO3':
this_play.ab, this_play.hit, this_play.error, this_play.double = 1, 1, 1, 1
if error_result == 1:
this_play.batter_final = 3
else:
this_play.batter_final = 4
this_play = advance_runners(session, this_play, num_bases=4, earned_bases=2)
elif hit_result == 'TR' and error_result is None:
this_play = await triples(session, interaction, this_play)
else:
this_play.ab, this_play.hit, this_play.error, this_play.triple = 1, 1, 1, 1
this_play = advance_runners(session, this_play, num_bases=4, earned_bases=3)
session.add(this_play)
session.commit()
session.refresh(this_play)
return this_play
def activate_last_play(session: Session, this_game: Game) -> Play:
logger.info(f'Pulling last play to complete and advance')
p_query = session.exec(select(Play).where(Play.game == this_game).order_by(Play.id.desc()).limit(1)).all()
logger.info(f'last play: {p_query[0].id}')
this_play = complete_play(session, p_query[0])
return this_play
def undo_play(session: Session, this_play: Play):
this_game = this_play.game
after_play_min = max(1, this_play.play_num - 2)
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)))
new_player_ids = []
new_players = session.exec(select(Lineup).where(Lineup.game == this_game, Lineup.after_play >= after_play_min)).all()
logger.info(f'Subs to roll back: {new_players}')
for lineup in new_players:
new_players.append(lineup.id)
old_player = session.get(Lineup, lineup.replacing_id)
old_player.active = True
session.add(old_player)
logger.warning(f'Deleting lineup IDs: {new_player_ids}')
session.exec(delete(Lineup).where(Lineup.id.in_(new_player_ids)))
session.commit()
try:
logger.info(f'Attempting to initialize play for Game {this_game.id}...')
this_play = this_game.initialize_play(session)
logger.info(f'Initialized play: {this_play.id}')
except PlayInitException:
logger.info(f'Plays found, attempting to active the last play')
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
)
logger.info(f'getting pooper string')
poop_string = ''
if 'pooper' in game_summary and game_summary['pooper'] is not None:
if isinstance(game_summary['pooper'], dict):
all_poop = [game_summary['pooper']]
elif isinstance(game_summary['pooper'], list):
all_poop = game_summary['pooper']
for line in all_poop:
player_name = f'{get_player_name_from_dict(line['player'])}'
poop_line = f'{player_name} - '
if 'hr' in line:
poop_line += f'{line["hit"]}-{line["ab"]}'
else:
poop_line += f'{line["ip"]} IP, {line["run"]} R'
if tp['run'] != line['e_run']:
poop_line += f' ({line["e_run"]} ER)'
poop_line += f', {line["hit"]} H, {line["so"]} K'
poop_line += f', {line["re24"]:.2f} re24\n'
poop_string += poop_line
if len(poop_string) > 0:
game_embed.add_field(
name='Pooper of the Game',
value=poop_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}')
async def update_game_settings(session: Session, interaction: discord.Interaction, this_game: Game, roll_buttons: bool = None, auto_roll: bool = None) -> discord.Embed:
if roll_buttons is not None:
this_game.roll_buttons = roll_buttons
if auto_roll is not None:
this_game.auto_roll = auto_roll
session.add(this_game)
session.commit()
session.refresh(this_game)
this_team = this_game.away_team if this_game.away_team.gmid == interaction.user.id else this_game.home_team
embed = this_team.embed
embed.title = f'Game Settings - {this_team.lname}'
embed.add_field(
name='Roll Buttons',
value=f'{"ON" if this_game.roll_buttons else "OFF"}'
)
embed.add_field(
name='Auto Roll',
value=f'{"ON" if this_game.auto_roll else "OFF"}'
)
return embed
async def manual_end_game(session: Session, interaction: discord.Interaction, this_game: Game, current_play: Play):
logger.info(f'manual_end_game - Game {this_game.id}')
GAME_DONE_STRING = 'Okay, it\'s gone. You\'re free to start another one!'
GAME_STAYS_STRING = 'No problem, this game will continue!'
if not is_game_over(current_play):
logger.info(f'manual_end_game - game is not over')
if current_play.inning_num == 1 and current_play.play_num < 3 and 'gauntlet' not in this_game.game_type.lower():
logger.info(f'manual_end_game - {this_game.game_type} game just started, asking for confirmation')
await interaction.edit_original_response(content='Looks like this game just started.')
cancel_early = await ask_confirm(
interaction,
'Are you sure you want to cancel it?',
label_type='yes',
timeout=30,
delete_question=False
)
if cancel_early:
logger.info(f'{interaction.user.name} is cancelling the game')
await interaction.channel.send(content=GAME_DONE_STRING)
news_ticker = get_channel(interaction, 'pd-network-news')
if news_ticker is not None:
await news_ticker.send(content=f'{interaction.user.display_name} had dinner plans so had to end their game down in {interaction.channel.mention} early.')
this_game.active = False
session.add(this_game)
session.commit()
else:
logger.info(f'{interaction.user.name} is not cancelling the game')
await interaction.channel.send_message(content=GAME_STAYS_STRING)
return
else:
logger.info(f'manual_end_game - {this_game.game_type} game currently in inning #{current_play.inning_num}, asking for confirmation')
await interaction.edit_original_response(content='It doesn\'t look like this game isn\'t over, yet. I can end it, but no rewards will be paid out and you will take the L.')
forfeit_game = await ask_confirm(
interaction,
'Should I end this game?',
label_type='yes',
timeout=30,
delete_question=False
)
if forfeit_game:
logger.info(f'{interaction.user.name} is forfeiting the game')
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'] = current_play.away_score
game_data['home_score'] = current_play.home_score
game_data['forfeit'] = True
try:
db_game = await db_post('games', payload=game_data)
except Exception as e:
logger.error(f'Unable to post forfeited game')
await interaction.channel.send(content=GAME_DONE_STRING)
news_ticker = get_channel(interaction, 'pd-network-news')
if news_ticker is not None:
await news_ticker.send(content=f'{interaction.user.display_name} escorts the {this_game.human_team.sname} out of {interaction.channel.mention} in protest.')
this_game.active = False
session.add(this_game)
session.commit()
else:
logger.info(f'{interaction.user.name} is not forfeiting the game')
await interaction.channel.send(content=GAME_STAYS_STRING)
return
# else:
# logger.info(f'manual_end_game - gauntlet game currently in inning #{current_play.inning_num}, asking for confirmation')
# await interaction.edit_original_response(content='It doesn\'t look like this game is over, yet. I can end it, but no rewards will be paid out and you will take the L.')
else:
logger.info(f'manual_end_game - game is over')
await complete_game(session, interaction, current_play)
async def groundballs(session: Session, interaction: discord.Interaction, this_play: Play, groundball_letter: Literal['a', 'b', 'c']):
if this_play.on_base_code == 2 and groundball_letter in ['a', 'b']:
logger.info(f'Groundball {groundball_letter} with runner on second')
to_right_side = await ask_confirm(
interaction,
question=f'Was that ball hit to either 1B or 2B?',
label_type='yes'
)
this_play = gb_result_6(session, this_play, to_right_side)
elif this_play.on_base_code in [3, 6] and groundball_letter in ['a', 'b']:
logger.info(f'Groundball {groundball_letter} with runner on third')
def_alignment = this_play.managerai.defense_alignment(session, this_play.game)
if this_play.game.ai_team is not None and this_play.pitcher.team.is_ai and def_alignment.infield_in:
logger.info(f'AI on defense, hit to MIF, playing in')
this_play = gb_result_7(session, this_play)
else:
logger.info(f'Checking if hit to MIF')
to_mif = await ask_confirm(
interaction,
question=f'Was that ball hit to either 2B or SS?',
label_type='yes'
)
if not to_mif:
logger.info(f'Not to a MIF, gb 7')
this_play = gb_result_7(session, this_play)
else:
logger.info(f'AI batting, hit to MIF')
mif_playing_in = await ask_confirm(
interaction,
question=f'Were they playing in?',
label_type='yes',
)
if mif_playing_in:
logger.info(f'playing in, gb 7')
this_play = gb_result_7(session, this_play)
else:
logger.info(f'playing back, gb 5')
this_play = gb_result_5(session, this_play, to_mif)
else:
this_play = await gb_letter(session, interaction, this_play, groundball_letter.upper(), 'None', False)
session.add(this_play)
session.commit()
session.refresh(this_play)
return this_play
async def gb_letter(session: Session, interaction: discord.Interaction, this_play: Play, groundball_letter: Literal['A', 'B', 'C'], position: str, defender_is_in: bool):
"""
Commits this_play
"""
if not defender_is_in:
if this_play.on_base_code == 0:
return await gb_result(session, interaction, this_play, 1)
elif groundball_letter == 'C':
return await gb_result(session, interaction, this_play, 3)
elif groundball_letter == 'A' and this_play.on_base_code in [1, 4, 5, 7]:
return await gb_result(session, interaction, this_play, 2)
elif groundball_letter == 'B' and this_play.on_base_code in [1, 4, 5, 7]:
return await gb_result(session, interaction, this_play, 4)
elif this_play.on_base_code in [3, 6]:
return await gb_result(session, interaction, this_play, 5, to_mif=position in ['2B', 'SS'])
else:
return await gb_result(session, interaction, this_play, 6, to_right_side=position in ['1B', '2B'])
else:
if groundball_letter == 'A' and this_play.on_base_code == 7:
return await gb_result(session, interaction, this_play, 10)
elif groundball_letter == 'B' and this_play.on_base_code == 5:
return await gb_result(session, interaction, this_play, 9)
elif this_play.on_base_code == 7:
return await gb_result(session, interaction, this_play, 11)
elif groundball_letter == 'A':
return await gb_result(session, interaction, this_play, 7)
elif groundball_letter == 'B':
return await gb_result(session, interaction, this_play, 1)
else:
return await gb_result(session, interaction, this_play, 8)
async def gb_result(session: Session, interaction: discord.Interaction, this_play: Play, groundball_result: int, to_mif: bool = None, to_right_side: bool = None):
"""
Commits this_play
Result 5 requires to_mif
Result 6 requires to_right_side
"""
logger.info(f'Starting a groundball result: GB #{groundball_result}, to_mif: {to_mif}, to_right_side: {to_right_side}')
if groundball_result == 1:
this_play = gb_result_1(session, this_play)
elif groundball_result == 2:
this_play = gb_result_2(session, this_play)
elif groundball_result == 3:
this_play = gb_result_3(session, this_play)
elif groundball_result == 4:
this_play = gb_result_4(session, this_play)
elif groundball_result == 5:
this_play = gb_result_5(session, this_play, to_mif)
elif groundball_result == 6:
this_play = gb_result_6(session, this_play, to_right_side)
elif groundball_result == 7:
this_play = gb_result_7(session, this_play)
elif groundball_result == 8:
this_play = gb_result_8(session, this_play)
elif groundball_result == 9:
this_play = gb_result_9(session, this_play)
elif groundball_result == 10:
this_play = gb_result_10(session, this_play)
elif groundball_result == 11:
this_play = gb_result_11(session, this_play)
elif groundball_result == 12:
this_play = await gb_result_12(session, this_play, interaction)
elif groundball_result == 13:
this_play = gb_result_13(session, this_play)
session.add(this_play)
session.commit()
session.refresh(this_play)
return this_play
def gb_result_1(session: Session, this_play: Play):
logger.info(f'GB 1')
this_play = advance_runners(session, this_play, 0)
this_play.ab, this_play.outs = 1, 1
return this_play
def gb_result_2(session: Session, this_play: Play):
logger.info(f'GB 2')
num_outs = 2 if this_play.starting_outs <= 1 else 1
this_play.ab, this_play.outs = 1, num_outs
this_play.on_first_final = None
if num_outs + this_play.starting_outs < 3:
if this_play.on_second:
this_play.on_second_final = 3
if this_play.on_third:
this_play.on_third_final = 4
log_run_scored(session, runner=this_play.on_third, this_play=this_play)
return this_play
def gb_result_3(session: Session, this_play: Play):
logger.info(f'GB 3')
if this_play.starting_outs < 2:
this_play = advance_runners(session, this_play, 1)
this_play.ab, this_play.outs = 1, 1
return this_play
def gb_result_4(session: Session, this_play: Play):
logger.info(f'GB 4')
if this_play.starting_outs < 2:
this_play = advance_runners(session, this_play, 1)
this_play.ab, this_play.outs = 1, 1
this_play.on_first_final = None
this_play.batter_final = 1
return this_play
def gb_result_5(session: Session, this_play: Play, to_mif: bool):
logger.info(f'GB 5')
this_play.ab, this_play.outs = 1, 1
if to_mif:
this_play = gb_result_3(session, this_play)
else:
this_play = gb_result_1(session, this_play)
return this_play
def gb_result_6(session: Session, this_play: Play, to_right_side: bool):
logger.info(f'GB 6')
this_play.ab, this_play.outs = 1, 1
if to_right_side:
this_play = gb_result_3(session, this_play)
else:
this_play = gb_result_1(session, this_play)
return this_play
def gb_result_7(session: Session, this_play: Play):
logger.info(f'GB 7')
this_play.ab, this_play.outs = 1, 1
this_play = advance_runners(session, this_play, num_bases=1, only_forced=True)
return this_play
def gb_result_8(session: Session, this_play: Play):
logger.info(f'GB 8')
return gb_result_7(session, this_play)
def gb_result_9(session: Session, this_play: Play):
logger.info(f'GB 9')
this_play.ab, this_play.outs = 1, 1
this_play.on_third_final = 3
this_play.on_first_final = 2
return this_play
def gb_result_10(session: Session, this_play: Play):
logger.info(f'GB 10')
num_outs = 2 if this_play.starting_outs <= 1 else 1
this_play.ab, this_play.outs = 1, num_outs
this_play.on_second_final = 3
this_play.on_first_final = 2
return this_play
def gb_result_11(session: Session, this_play: Play):
logger.info(f'GB 11')
this_play.ab, this_play.outs = 1, 1
this_play.on_first_final = 2
this_play.on_second_final = 3
this_play.batter_final = 1
return this_play
async def gb_decide(session: Session, this_play: Play, interaction: discord.Interaction):
logger.info(f'GB Decide')
runner = this_play.on_third if this_play.on_third is not None else this_play.on_second
logger.info(f'runner: {runner}')
pos_rating = await get_position(session, this_play.defender.card, this_play.check_pos)
safe_range = runner.card.batterscouting.battingcard.running - 4 + pos_rating.range
advance_base = 4 if this_play.on_third is not None else 3
logger.info(f'pos_rating: {pos_rating}\nsafe_range: {safe_range}\nadvance_base: {advance_base}')
if this_play.game.ai_team is not None and this_play.ai_is_batting:
run_resp = this_play.managerai.gb_decide_run(session, this_play.game)
if this_play.on_second is None and this_play.on_third is None:
log_exception(InvalidResultException, 'Cannot run GB Decide without a runner on base.')
if safe_range >= run_resp.min_safe:
is_lead_running = True
else:
is_lead_running = False
else:
is_lead_running = await ask_confirm(
interaction,
f'Is **{runner.card.player.name}** attempting to advance {TO_BASE[advance_base]} with a **1-{safe_range}** safe range?',
label_type='yes',
delete_question=False
)
if not is_lead_running:
this_play = advance_runners(session, this_play, 0)
this_play.outs = 1
else:
if this_play.game.ai_team is not None and not this_play.ai_is_batting:
throw_resp = this_play.managerai.gb_decide_throw(
session,
this_play.game,
runner_speed=runner.card.batterscouting.battingcard.running,
defender_range=pos_rating.range
)
throw_for_lead = throw_resp.at_lead_runner
await interaction.channel.send(
content=f'**{this_play.defender.player.name}** is {"not " if not throw_for_lead else ""}throwing for {runner.player.name}{"!" if throw_for_lead else "."}'
)
else:
throw_for_lead = await ask_confirm(
interaction,
f'Is {this_play.defender.player.name} throwing for {runner.player.name}?',
label_type='yes'
)
if not throw_for_lead:
if this_play.starting_outs < 2:
this_play = advance_runners(session, this_play, 1)
this_play.outs = 1
else:
is_lead_out = await ask_confirm(
interaction,
f'Was {runner.card.player.name} thrown out?',
custom_confirm_label='Thrown Out',
custom_cancel_label='Safe'
)
if is_lead_out:
this_play.outs = 1
this_play.batter_final = 1
if this_play.on_third:
this_play.on_first_final = 2 if this_play.on_first is not None else 0
this_play.on_second_final = 3 if this_play.on_second is not None else 0
elif this_play.on_second:
this_play.on_first_final = 2 if this_play.on_first is not None else 0
else:
this_play = advance_runners(session, this_play, num_bases=1)
this_play.batter_final = 1
return this_play
async def gb_result_12(session: Session, this_play: Play, interaction: discord.Interaction):
logger.info(f'GB 12')
if this_play.check_pos in ['1B', '2B']:
return gb_result_3(session, this_play)
elif this_play.check_pos == '3B':
return gb_result_1(session, this_play)
else:
return await gb_decide(session, this_play, interaction)
def gb_result_13(session: Session, this_play: Play):
logger.info(f'GB 13')
if this_play.check_pos in ['C', '3B']:
num_outs = 2 if this_play.starting_outs <= 1 else 1
this_play.ab, this_play.outs = 1, num_outs
this_play.batter_final = 1
else:
this_play = gb_result_2(session, this_play)
return this_play
async def new_game_conflicts(session: Session, interaction: discord.Interaction):
conflict = get_channel_game_or_none(session, interaction.channel_id)
if conflict is not None:
await interaction.edit_original_response(
content=f'Ope. There is already a game going on in this channel. Please wait for it to complete '
f'before starting a new one.'
)
log_exception(GameException, f'{interaction.user} attempted to start a new game in {interaction.channel.name}, but there is another active game')
if interaction.channel.category is None or interaction.channel.category.name != PUBLIC_FIELDS_CATEGORY_NAME:
await interaction.edit_original_response(
content=f'Why don\'t you head down to one of the Public Fields that way other humans can help if anything pops up?'
)
log_exception(GameException, f'{interaction.user} attempted to start a new game in {interaction.channel.name} so they were redirected to {PUBLIC_FIELDS_CATEGORY_NAME}')