paper-dynasty-discord/command_logic/logic_gameplay.py
Cal Corum c4577ed46f
Some checks failed
Build Docker Image / build (pull_request) Failing after 16s
fix: validate player positions in lineup before game start
Prevents PositionNotFoundException from crashing mlb-campaign when a
player is placed at a position they cannot play (e.g. an outfielder
listed at Catcher in the Google Sheet). Adds early validation in
get_lineups_from_sheets and proper error handling at all read_lineup
call sites so the user gets a clear message and the game is cleaned up.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 19:32:27 -06:00

5175 lines
185 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, or_, 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 play_lock import release_play_lock, safe_play_lock
from gauntlets import post_result
from helpers import (
COLORS,
DEFENSE_LITERAL,
DEFENSE_NO_PITCHER_LITERAL,
SBA_COLOR,
get_channel,
position_name_to_abbrev,
team_role,
)
from in_game.ai_manager import get_starting_lineup
from in_game.game_helpers import PUBLIC_FIELDS_CATEGORY_NAME, legal_check
from in_game.gameplay_models import (
BattingCard,
Card,
Game,
Lineup,
PositionRating,
RosterLink,
Team,
Play,
)
from in_game.gameplay_queries import (
get_active_games_by_team,
get_available_batters,
get_batter_card,
get_batting_statline,
get_game_cardset_links,
get_or_create_ai_card,
get_pitcher_runs_by_innings,
get_pitching_statline,
get_plays_by_pitcher,
get_position,
get_available_pitchers,
get_card_or_none,
get_channel_game_or_none,
get_db_ready_decisions,
get_db_ready_plays,
get_game_lineups,
get_last_team_play,
get_one_lineup,
get_player_id_from_dict,
get_player_name_from_dict,
get_player_or_none,
get_sorted_lineups,
get_team_or_none,
get_players_last_pa,
post_game_rewards,
)
from in_game.managerai_responses import DefenseResponse
from utilities.buttons import ButtonOptions, Confirm, ask_confirm, ask_with_buttons
from utilities.dropdown import (
DropdownView,
SelectBatterSub,
SelectDefensiveChange,
SelectReliefPitcher,
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"}
def safe_wpa_lookup(
inning_half: str,
inning_num: int,
starting_outs: int,
on_base_code: int,
run_diff: int,
) -> float:
"""
Safely lookup win probability from WPA_DF with fallback logic for missing keys.
Fallback strategy:
1. Try exact lookup
2. Try with simplified on_base_code (reduce to bases empty state)
3. Return 0.0 if no data available
Args:
inning_half: 'top' or 'bot'
inning_num: Inning number (1-9)
starting_outs: Number of outs (0-2)
on_base_code: Base occupation code (0-7)
run_diff: Home run differential (-6 to 6)
Returns:
float: Home team win expectancy (0.0 to 1.0)
"""
# Construct the primary key
key = f"{inning_half}_{inning_num}_{starting_outs}_out_{on_base_code}_obc_{run_diff}_home_run_diff"
# Try exact lookup first
try:
return float(WPA_DF.loc[key, "home_win_ex"])
except KeyError:
logger.warning(f"WPA key not found: {key}, attempting fallback")
# Fallback 1: Try with simplified on_base_code (bases empty = 0)
if on_base_code != 0:
fallback_key = f"{inning_half}_{inning_num}_{starting_outs}_out_0_obc_{run_diff}_home_run_diff"
try:
result = float(WPA_DF.loc[fallback_key, "home_win_ex"])
logger.info(
f"WPA fallback successful using bases empty state: {fallback_key}"
)
return result
except KeyError:
logger.warning(f"WPA fallback key not found: {fallback_key}")
# Fallback 2: Return 0.0 if no data available
logger.warning(f"WPA using fallback value 0.0 for missing key: {key}")
return 0.0
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,
live_scorecard: bool = False,
) -> 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}")
curr_play = this_game.current_play_or_none(session)
embed = discord.Embed(
title=f"{this_game.away_team.sname} @ {this_game.home_team.sname}{gt_string}"
)
if curr_play is None:
logger.info(
f"There is no play in game {this_game.id}, trying to initialize play"
)
try:
curr_play = this_game.initialize_play(session)
except LineupsMissingException as e:
logger.info(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}")
if curr_play.pitcher.is_fatigued:
embed_color = COLORS["red"]
elif curr_play.pitcher.after_play == curr_play.play_num - 1:
embed_color = COLORS["white"]
elif curr_play.is_new_inning:
embed_color = COLORS["yellow"]
else:
embed_color = COLORS["sba"]
embed.color = embed_color
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 curr_play.pitcher.is_fatigued:
pit_string += f"\n***F A T I G U E D***"
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: {'+' if catcher_rating.arm > 0 else ''}{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 and not live_scorecard:
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, with_links=False
),
)
embed.add_field(
name=f"{this_game.home_team.abbrev} Lineup",
value=this_game.team_lineup(
session, this_game.home_team, with_links=False
),
)
else:
logger.info(f"There is no play in game {this_game.id}, posting lineups")
logger.info(f"Pulling away lineup")
embed.add_field(
name=f"{this_game.away_team.abbrev} Lineup",
value=this_game.team_lineup(session, this_game.away_team),
)
logger.info(f"Pulling home lineup")
embed.add_field(
name=f"{this_game.home_team.abbrev} Lineup",
value=this_game.team_lineup(session, this_game.home_team),
)
return embed
async def new_game_checks(
session: Session,
interaction: discord.Interaction,
away_team_abbrev: str,
home_team_abbrev: str,
):
try:
logger.info(f"Checking for game conflicts in {interaction.channel.name}")
await new_game_conflicts(session, interaction)
except GameException as e:
return None
logger.info(f"Getting teams")
away_team = await get_team_or_none(session, team_abbrev=away_team_abbrev)
if away_team is None:
logger.error(f"Away team not found")
await interaction.edit_original_response(
content=f"Hm. I'm not sure who **{away_team_abbrev}** is - check on that and try again!"
)
return None
home_team = await get_team_or_none(session, team_abbrev=home_team_abbrev)
if home_team is None:
logger.error(f"Home team not found")
await interaction.edit_original_response(
content=f"Hm. I'm not sure who **{home_team_abbrev}** is - check on that and try again!"
)
return None
human_team = away_team if home_team.is_ai else home_team
logger.info(f"Checking for other team games")
conflict_games = get_active_games_by_team(session, team=human_team)
if len(conflict_games) > 0:
logger.error(
f"Conflict creating a new game in channel: {interaction.channel.name}"
)
await interaction.edit_original_response(
content=f"Ope. The {human_team.sname} are already playing over in {interaction.guild.get_channel(conflict_games[0].channel_id).mention}"
)
return None
if interaction.user.id not in [away_team.gmid, home_team.gmid]:
if interaction.user.id != 258104532423147520:
await interaction.edit_original_response(
content="You can only start a new game if you GM one of the teams."
)
return None
else:
await interaction.channel.send(
"Sigh. Cal is cheating again starting a game for someone else."
)
return {"away_team": away_team, "home_team": home_team}
def starting_pitcher_dropdown_view(
session: Session,
this_game: Game,
human_team: Team,
game_type: str = None,
responders: list[discord.User] = None,
):
pitchers = get_available_pitchers(
session, this_game, human_team, sort="starter-desc"
)
logger.info(f"sorted pitchers: {pitchers}")
if len(pitchers) == 0:
log_exception(MissingRosterException, "No pitchers were found to select SP")
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",
responders=responders,
)
return DropdownView(dropdown_objects=[sp_selection])
def relief_pitcher_dropdown_view(
session: Session,
this_game: Game,
human_team: Team,
batting_order: int,
responders: list[discord.User] = None,
):
pitchers = get_available_pitchers(session, this_game, human_team)
logger.info(f"sorted pitchers: {pitchers}")
if len(pitchers) == 0:
log_exception(MissingRosterException, "No pitchers were found to select RP")
rp_selection = SelectReliefPitcher(
this_game=this_game,
this_team=human_team,
batting_order=batting_order,
session=session,
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 relief pitcher",
responders=responders,
)
return DropdownView(dropdown_objects=[rp_selection])
async def defender_dropdown_view(
session: Session,
this_game: Game,
human_team: Team,
new_position: DEFENSE_NO_PITCHER_LITERAL,
responders: list[discord.User] = None,
):
active_players = get_game_lineups(session, this_game, human_team, is_active=True)
first_pass = [
x for x in active_players if x.position != "P" and x.batting_order != 10
]
if len(first_pass) == 0:
log_exception(
MissingRosterException,
"No active defenders were found to make defensive change",
)
defender_list = []
for x in first_pass:
this_pos = session.exec(
select(PositionRating).where(
PositionRating.player_id == x.player.id,
PositionRating.position == position_name_to_abbrev(new_position),
PositionRating.variant == x.card.variant,
)
).all()
if len(this_pos) > 0:
defender_list.append(x)
if len(defender_list) == 0:
log_exception(
PlayerNotFoundException,
f"I dont see any legal defenders for {new_position} on the field.",
)
defender_selection = SelectDefensiveChange(
this_game=this_game,
this_team=human_team,
new_position=new_position,
session=session,
responders=responders,
placeholder=f"Who is moving to {new_position}?",
options=[
SelectOption(label=f"{x.player.name_with_desc}", value=x.id)
for x in defender_list
],
)
return DropdownView(dropdown_objects=[defender_selection])
def sub_batter_dropdown_view(
session: Session,
this_game: Game,
human_team: Team,
batting_order: int,
responders: list[discord.User],
):
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",
responders=responders,
)
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 `/substitute` 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":
try:
await get_position(session, batter.card, batter.position)
except PositionNotFoundException:
raise PositionNotFoundException(
f"Could not find {batter.position} ratings for **{batter.player.name_with_desc}**. "
f"Please check your lineup in Google Sheets and make sure each player is at a valid 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 = safe_wpa_lookup(
next_play.inning_half,
inning_num,
next_play.starting_outs,
next_play.on_base_code,
new_rd,
)
# 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 = safe_wpa_lookup(
this_play.inning_half,
inning_num,
this_play.starting_outs,
this_play.on_base_code,
old_rd,
)
# 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")
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
logger.info(f"Calculate runs scored")
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:
logger.info(f"{this_runner} advances to first")
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",
)
if not switch_sides:
on_first = this_runner
elif runner_dest == 2:
logger.info(f"{this_runner} advances to second")
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",
)
if not switch_sides:
on_second = this_runner
elif runner_dest == 3:
logger.info(f"{this_runner} advances to third")
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",
)
if not switch_sides:
on_third = this_runner
elif runner_dest == 4:
logger.info(f"{this_runner} advances to home")
runs_scored += 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
logger.info(f"Calculate runs scored")
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:
logger.info(f"{this_runner} advances to first")
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",
)
if not switch_sides:
on_first = this_runner
elif runner_dest == 2:
logger.info(f"{this_runner} advances to second")
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",
)
if not switch_sides:
on_second = this_runner
elif runner_dest == 3:
logger.info(f"{this_runner} advances to third")
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",
)
if not switch_sides:
on_third = this_runner
elif runner_dest == 4:
logger.info(f"{this_runner} advances to home")
runs_scored += 1
obc = get_obc(on_first, on_second, on_third)
if this_play.inning_half == "top":
away_score = this_play.away_score + runs_scored
home_score = this_play.home_score
logger.info(f"Check for go-ahead run")
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
logger.info(f"Check for go-ahead run")
if (
runs_scored > 0
and this_play.home_score <= this_play.away_score
and home_score > away_score
):
this_play.is_go_ahead = True
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_pitcher = get_one_lineup(
session, this_play.game, new_pitcher_team, position="P"
)
logger.info(f"Check for {new_pitcher.player.name} POW")
outs = session.exec(
select(func.sum(Play.outs)).where(
Play.game == this_play.game, Play.pitcher == new_pitcher
)
).one()
if outs is None:
outs = 0
logger.info(f"Outs recorded: {outs}")
if new_pitcher.replacing_id is None:
pow_outs = new_pitcher.card.pitcherscouting.pitchingcard.starter_rating * 3
logger.info(f"Using starter rating, POW outs: {pow_outs}")
else:
pow_outs = new_pitcher.card.pitcherscouting.pitchingcard.relief_rating * 3
logger.info(f"Using relief rating, POW outs: {pow_outs}")
if not new_pitcher.is_fatigued:
logger.info(f"Pitcher is not currently fatigued")
if outs >= pow_outs:
logger.info(f"Pitcher is beyond POW - adding fatigue")
new_pitcher.is_fatigued = True
elif new_pitcher.replacing_id is None:
logger.info(f"Pitcher is not in POW yet")
total_runs = session.exec(
select(func.count(Play.id)).where(
Play.game == this_play.game,
Play.pitcher == new_pitcher,
Play.run == 1,
)
).one()
logger.info(
f"Runs allowed by {new_pitcher.player.name_with_desc}: {total_runs}"
)
if total_runs >= 5:
if total_runs >= 7:
logger.info(f"Starter has allowed 7+ runs - adding fatigue")
new_pitcher.is_fatigued = True
else:
last_two = [
x
for x in range(
this_play.inning_num, this_play.inning_num - 2, -1
)
if x > 0
]
runs_last_two = get_pitcher_runs_by_innings(
session, this_play.game, new_pitcher, last_two
)
logger.info(f"Runs allowed last two innings: {runs_last_two}")
if runs_last_two >= 6:
logger.info(
f"Starter has allowed at least six in the last two - adding fatigue"
)
new_pitcher.is_fatigued = True
else:
runs_this_inning = get_pitcher_runs_by_innings(
session, this_play.game, new_pitcher, [this_play.inning_num]
)
logger.info(f"Runs allowed this inning: {runs_this_inning}")
if runs_this_inning >= 5:
logger.info(
f"Starter has allowed at least five this inning - adding fatigue"
)
new_pitcher.is_fatigued = True
session.add(new_pitcher)
if outs >= (pow_outs - 3):
in_pow = True
if not new_pitcher.is_fatigued:
logger.info(f"checking for runners in POW")
runners_in_pow = session.exec(
select(func.count(Play.id)).where(
Play.game == this_play.game,
Play.pitcher == new_pitcher,
Play.in_pow == True,
or_(Play.hit == 1, Play.bb == 1),
Play.ibb == 0,
)
).one() # change to hits and walks
logger.info(f"runners in pow: {runners_in_pow}")
if runners_in_pow >= 3:
new_pitcher.is_fatigued = True
else:
in_pow = False
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=new_pitcher,
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,
in_pow=in_pow,
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."
)
position = row[0].upper()
if position != "DH":
player_positions = [
getattr(this_card.player, f"pos_{i}") for i in range(1, 9)
if getattr(this_card.player, f"pos_{i}") is not None
]
if position not in player_positions:
raise PositionNotFoundException(
f"**{this_card.player.name_with_desc}** (card {this_card.id}) is listed at **{position}** in your lineup, "
f"but can only play {', '.join(player_positions)}. Please fix your lineup in Google Sheets."
)
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, TypeError) as e:
logger.error(f"Could not pull roster for {this_team.abbrev}: {e}")
logger.error(f"raw_cells type: {type(raw_cells)} / raw_cells: {raw_cells}")
raise GoogleSheetsException(
f"Uh oh. I had trouble reading your roster from Google Sheets (range {l_range}). Please make sure your roster is saved properly and all cells contain valid card IDs."
)
for x in card_ids:
this_card = await get_card_or_none(session, card_id=x)
if this_card is None:
logger.error(
f"Card ID {x} not found while loading roster for team {this_team.abbrev}"
)
raise CardNotFoundException(
f"Card ID {x} was not found in your collection. Please check your roster sheet and make sure all card IDs are valid."
)
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, lock_play: bool = True
) -> tuple[Game, Team, Play]:
"""
Validates interaction permissions and optionally locks the current play for processing.
Args:
session: Database session
interaction: Discord interaction
command_name: Name of the command being executed
lock_play: If True (default), locks the play. Set to False for read-only commands.
IMPORTANT: If lock_play=True, the calling code MUST either:
1. Call complete_play() which releases the lock on success, OR
2. Use exception handling to call release_play_lock() on failure
Commits this_play with locked=True (only if lock_play=True)
"""
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)
if lock_play:
# Only check and set lock if this is a write command
if this_play.locked:
logger.warning(
f"{interaction.user.name} attempted {command_name} on locked play {this_play.id} "
f"in game {this_game.id}. Rejecting duplicate submission."
)
from exceptions import PlayLockedException
raise PlayLockedException(
"This play is already being processed. Please wait for the current action to complete.\n\n"
"If this play appears stuck, go call dad."
)
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 create_pinch_runner_entry_play(
session: Session, game: Game, current_play: Play, pinch_runner_lineup: Lineup
) -> Play:
"""
Creates a "pinch runner entry" Play record when a pinch runner substitutes for a player on base.
This Play has PA=0, AB=0 so it doesn't affect counting stats, but provides a record to mark
when the pinch runner scores.
Commits the new Play.
Args:
session: Database session
game: The current game
current_play: The current active Play (not yet complete)
pinch_runner_lineup: The Lineup record for the pinch runner
Returns:
The newly created entry Play
"""
logger.info(
f"Creating pinch runner entry Play for {pinch_runner_lineup.player.name_with_desc} in Game {game.id}"
)
# Get the next play number
max_play_num = session.exec(
select(func.max(Play.play_num)).where(Play.game == game)
).one()
next_play_num = max_play_num + 1 if max_play_num else 1
# Create the entry Play
entry_play = Play(
game=game,
play_num=next_play_num,
batter=pinch_runner_lineup,
pitcher=current_play.pitcher,
catcher=current_play.catcher,
inning_half=current_play.inning_half,
inning_num=current_play.inning_num,
batting_order=pinch_runner_lineup.batting_order,
starting_outs=current_play.starting_outs,
away_score=current_play.away_score,
home_score=current_play.home_score,
on_base_code=current_play.on_base_code,
batter_pos=pinch_runner_lineup.position,
# This is NOT a plate appearance
pa=0,
ab=0,
run=0, # Will be set to 1 if they score
# All other stats default to 0
complete=True, # This "event" is complete
is_tied=current_play.is_tied,
is_go_ahead=False,
is_new_inning=False,
managerai_id=current_play.managerai_id,
)
session.add(entry_play)
session.commit()
session.refresh(entry_play)
logger.info(
f"Created entry Play #{entry_play.play_num} for pinch runner {pinch_runner_lineup.player.name_with_desc}"
)
return entry_play
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)
runner_embed = this_play.batter.team.embed
runner_embed.title = f"{this_play.on_third.player.name} To Home"
runner_embed.description = f"{this_play.catcher.team.abbrev} C {this_play.catcher.player.name} Blocking the Plate"
runner_embed.add_field(
name="Catcher Range", value=catcher_rating.range
)
runner_embed.add_field(name="", value="", inline=False)
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
runner_embed.add_field(name="Safe Range", value=f"1 - {safe_range}")
out_range = f"{safe_range + 1}"
if safe_range < 19:
out_range += f" - 20"
runner_embed.add_field(name="Out Range", value=out_range)
await interaction.channel.send(content=None, embed=runner_embed)
await interaction.channel.send(
content=None, embeds=this_roll.embeds
)
if this_roll.d_twenty <= safe_range:
logger.info(
f"Roll of {this_roll.d_twenty} is SAFE {AT_BASE[4]}"
)
out_at_home = False
q_text = f"Looks like **{runner.name}** is SAFE {AT_BASE[4]}!"
else:
logger.info(f"Roll of {this_roll.d_twenty} is OUT {AT_BASE[4]}")
out_at_home = True
q_text = f"Looks like **{runner.name}** is OUT {AT_BASE[4]}!"
is_correct = await ask_confirm(
interaction=interaction,
question=f"{q_text} Is that correct?",
label_type="yes",
)
if not is_correct:
logger.info(
f"{interaction.user.name} says this result is incorrect - setting out_at_home to {not out_at_home}"
)
out_at_home = not out_at_home
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
@log_errors
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.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=interaction,
question=f"{q_text} Is that correct?",
label_type="yes",
)
if not is_correct:
logger.info(
f"{interaction.user.name} says this result is incorrect - setting runner_thrown_out to {not runner_thrown_out}"
)
runner_thrown_out = not runner_thrown_out
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
else:
logger.info(f"Runner is safe")
if this_play.on_first == trail_runner:
this_play.on_first_final += 1
if this_play.on_first_final == 4:
logger.info(f"Add an rbi")
this_play.rbi += 1
elif this_play.batter == trail_runner:
this_play.batter_final += 1
else:
log_exception(
LineupsMissingException,
f"Could not find trail runner to advance",
)
# 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")
if runner_thrown_out:
this_play.on_second_final = None
else:
this_play.on_second_final = lead_base
if lead_base == 4:
log_run_scored(session, this_play.on_second, this_play)
elif this_play.on_first == lead_runner:
logger.info(f"setting lead runner on_first")
if runner_thrown_out:
this_play.on_first_final = None
else:
this_play.on_first_final = lead_base
if lead_base == 4:
log_run_scored(session, this_play.on_first, this_play)
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
else:
logger.info(f"Runner is safe")
if this_play.on_first == trail_runner:
this_play.on_first_final += 1
if this_play.on_first_final == 4:
this_play.rbi += 1
elif this_play.batter == trail_runner:
this_play.batter_final += 1
else:
log_exception(
LineupsMissingException,
f"Could not find trail runner to advance",
)
# 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")
if runner_thrown_out:
this_play.on_second_final = None
else:
this_play.on_second_final = lead_base
if lead_base == 4:
log_run_scored(session, this_play.on_second, this_play)
elif this_play.on_first == lead_runner:
logger.info(f"setting lead runner on_first_final")
if runner_thrown_out:
this_play.on_first_final = None
else:
this_play.on_first_final = lead_base
if lead_base == 4:
log_run_scored(session, this_play.on_first, this_play)
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
if steal_type == "steal-plus-overthrow" and this_play.on_third:
this_play.on_third_final = 4
log_run_scored(session, this_play.on_third, this_play, is_earned=False)
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_rare_play: {is_rare_play} / is_correct: {is_correct}"
)
if not is_correct:
logger.error(f"{interaction.user.name} says the result was wrong.")
logger.info(f"Asking if there was a hit")
allow_hit = await ask_confirm(
interaction,
f"Did **{this_defender.player.name}** allow a hit?",
label_type="yes",
)
if allow_hit:
if position in ["1B", "2B", "3B", "SS", "P"]:
hit_options = ["SI1", "SI2"]
elif position in ["LF", "CF", "RF"]:
hit_options = ["SI2", "DO2", "DO3", "TR"]
else:
hit_options = ["SI1", "SPD"]
logger.info(f"Setting hit options to {hit_options}")
else:
if position in ["1B", "2B", "3B", "SS", "P"]:
hit_options = ["G3#", "G3", "G2#", "G2", "G1"]
elif position in ["LF", "CF", "RF"]:
hit_options = ["F1", "F2", "F3"]
else:
hit_options = ["G3", "G2", "G1", "PO", "FO"]
logger.info(f"Setting hit options to {hit_options}")
new_hit_result = await ask_with_buttons(
interaction,
button_options=hit_options,
question=f"Which result did **{this_defender.player.name}** allow?",
)
if new_hit_result is None:
logger.error(f"No hit result was returned")
return
logger.info(f"new hit result: {new_hit_result}")
logger.info(f"Asking if there was an error")
allow_error = await ask_confirm(
interaction,
f"Did **{this_defender.player.name}** commit an error?",
label_type="yes",
)
if allow_error:
if position in ["1B", "2B", "3B", "SS", "P", "C"]:
error_options = ["1 base", "2 bases"]
else:
error_options = ["1 base", "2 bases", "3 bases"]
logger.info(f"Setting error options to {error_options}")
new_error_result = await ask_with_buttons(
interaction,
button_options=error_options,
question=f"How many bases was **{this_defender.player.name}**'s error?",
)
if new_error_result is None:
logger.error(f"No error result was returned")
return
else:
if "1" in new_error_result:
new_error_result = 1
elif "2" in new_error_result:
new_error_result = 2
else:
new_error_result = 3
else:
new_error_result = None
logger.info(f"new_error_result: {new_error_result}")
logger.info(f"Setting hit and error results and continuing with processing.")
hit_result = new_hit_result
error_result = new_error_result
logger.info(
f'hit_result == "SPD" ({hit_result == "SPD"}) and not is_rare_play ({not is_rare_play})'
)
if hit_result == "SPD" and not is_rare_play:
logger.info(f"Non-rare play SPD check")
runner_speed = this_play.batter.card.batterscouting.battingcard.running
speed_embed = this_play.batter.team.embed
speed_embed.title = f"Catcher X-Check - Speed Check"
speed_embed.description = f"{this_play.batter.player.name} Speed Check"
speed_embed.add_field(name=f"Runner Speed", value=f"{runner_speed}")
speed_embed.add_field(name="", value="", inline=False)
speed_embed.add_field(name="Safe Range", value=f"1 - {runner_speed}")
speed_embed.add_field(name="Out Range", value=f"{runner_speed + 1} - 20")
this_roll = d_twenty_roll(this_play.batter.team, this_play.game)
if this_roll.d_twenty <= runner_speed:
result = "SAFE"
else:
result = "OUT"
logger.info(
f"SPD check roll: {this_roll.d_twenty} / runner_speed: {runner_speed} / result: {result}"
)
await interaction.channel.send(
content=None, embeds=[speed_embed, *this_roll.embeds]
)
is_correct = await ask_confirm(
interaction,
f"Looks like **{this_play.batter.player.name}** is {result} at first! Is that correct?",
label_type="yes",
)
if is_correct:
logger.info(f"Result is correct")
if result == "OUT":
hit_result = "G3"
else:
hit_result = "SI1"
else:
logger.info(f"Result is NOT correct")
if result == "OUT":
hit_result = "SI1"
else:
hit_result = "G3"
logger.info(f"Final SPD check result: {hit_result}")
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 = await 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.run,
this_play.triple,
this_play.batter_final,
) = 1, 1, 1, 1, 1, 4
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.play_num.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.play_num.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 x in new_players:
logger.info(f"Marking {x} for deletion")
new_player_ids.append(x.id)
old_player = session.get(Lineup, x.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,
responders=[interaction.user],
)
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:
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
final_inning = (
this_play.inning_num
if this_play.inning_half == "bot"
else this_play.inning_num - 1
)
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,
bot: discord.Client = None,
):
# 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,
)
if "gauntlet" in this_game.game_type:
logger.info(f"Posting gauntlet results")
await post_result(
run_id=int(this_game.game_type.split("-")[3]),
is_win=winning_team.gmid == interaction.user.id,
this_team=this_game.human_team,
bot=bot,
channel=interaction.channel,
responders=[interaction.user],
)
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.starting_outs == 2:
return await gb_result(session, interaction, this_play, 1)
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:
if def_alignment.infield_in:
logger.info(f"AI on defense, playing in")
if groundball_letter == "a":
this_play = gb_result_7(session, this_play)
else:
this_play = gb_result_1(session, this_play)
else:
logger.info(f"AI on defense, playing back")
to_mif = await ask_confirm(
interaction,
question=f"Was that ball hit to either 2B or SS?",
label_type="yes",
)
if to_mif or not def_alignment.corners_in:
logger.info(f"playing back, gb 5")
this_play = gb_result_5(session, this_play, to_mif)
else:
logger.info(f"corners in, gb 7")
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 to_mif:
playing_in = await ask_confirm(
interaction,
question=f"Were they playing in?",
label_type="yes",
)
if playing_in:
logger.info(f"To MIF, batter out, runners hold")
this_play = gb_result_1(session, this_play)
else:
logger.info(f"To MIF, playing back, gb 3")
this_play = gb_result_3(session, this_play)
else:
logger.info(f"Batter out, runners hold")
this_play = gb_result_1(session, this_play)
elif this_play.on_base_code in [5, 7] and groundball_letter == "a":
logger.info(f"Groundball {groundball_letter} with runners on including third")
if this_play.game.ai_team is not None and this_play.pitcher.team.is_ai:
def_alignment = this_play.managerai.defense_alignment(
session, this_play.game
)
logger.info(f"def_alignment: {def_alignment}")
if def_alignment.infield_in:
if this_play.on_base_code == 5:
logger.info(f"playing in, gb 7")
this_play = gb_result_7(session, this_play)
else:
logger.info(f"playing in, gb 10")
this_play = gb_result_10(session, this_play)
elif def_alignment.corners_in:
logger.info(f"Checking if ball was hit to 1B/3B")
to_cif = await ask_confirm(
interaction, f"Was that ball hit to 1B/3B?", label_type="yes"
)
if to_cif:
if this_play.on_base_code == 5:
logger.info(f"Corners in, gb 7")
this_play = gb_result_7(session, this_play)
else:
logger.info(f"Corners in, gb 10")
this_play = gb_result_10(session, this_play)
else:
logger.info(f"Corners back, gb 2")
this_play = gb_result_2(session, this_play)
else:
logger.info(f"playing back, gb 2")
this_play = gb_result_2(session, this_play)
else:
playing_in = await ask_confirm(
interaction, question="Was the defender playing in?", label_type="yes"
)
if playing_in and this_play.on_base_code == 5:
logger.info(f"playing in, gb 7")
this_play = gb_result_7(session, this_play)
elif playing_in:
logger.info(f"playing in, gb 10")
this_play = gb_result_10(session, this_play)
else:
logger.info(f"playing back, gb 2")
this_play = gb_result_2(session, this_play)
elif this_play.on_base_code in [5, 7] and groundball_letter == "b":
logger.info(f"Groundball {groundball_letter} with runners on including third")
playing_in = False
if this_play.game.ai_team is not None and this_play.pitcher.team.is_ai:
def_alignment = this_play.managerai.defense_alignment(
session, this_play.game
)
logger.info(f"def_alignment: {def_alignment}")
to_mif = await ask_confirm(
interaction, question="Was that hit to 2B/SS?", label_type="yes"
)
if def_alignment.infield_in or not to_mif and def_alignment.corners_in:
playing_in = True
else:
playing_in = await ask_confirm(
interaction, question="Was the defender playing in?", label_type="yes"
)
if playing_in and this_play.on_base_code == 7:
logger.info(f"playing in, gb 11")
this_play = gb_result_11(session, this_play)
elif playing_in:
logger.info(f"playing in, gb 9")
this_play = gb_result_9(session, this_play)
else:
logger.info(f"playing back, gb 4")
this_play = gb_result_4(session, this_play)
else:
if this_play.on_base_code in [3, 5, 6, 7]:
def_align = this_play.managerai.defense_alignment(session, this_play.game)
if def_align.infield_in:
playing_in = True
else:
to_mif = await ask_confirm(
interaction,
question="Was that ball hit to 2B/SS?",
label_type="yes",
)
if not to_mif and def_align.corners_in:
playing_in = True
else:
playing_in = False
else:
playing_in = False
this_play = await gb_letter(
session,
interaction,
this_play,
groundball_letter.upper(),
"None",
playing_in,
)
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}",
)
async def select_ai_reliever(session: Session, ai_team: Team, this_play: Play) -> Card:
logger.info(f"Selecting an AI reliever")
ai_score = (
this_play.away_score
if this_play.game.away_team_id == ai_team.id
else this_play.home_score
)
human_score = (
this_play.home_score
if this_play.game.away_team_id == ai_team.id
else this_play.away_score
)
logger.info(f"scores - ai: {ai_score} / human: {human_score}")
if abs(ai_score - human_score) >= 7:
need = "length"
elif this_play.inning_num >= 9 and abs(ai_score - human_score) <= 3:
need = "closer"
elif this_play.inning_num in [7, 8] and abs(ai_score - human_score) <= 3:
need = "setup"
elif abs(ai_score - human_score) <= 3:
need = "middle"
else:
need = "length"
logger.info(f"need: {need}")
used_pitchers = get_game_lineups(session, this_play.game, ai_team, is_active=False)
used_player_ids = [this_play.pitcher.player_id]
id_string = f"&used_pitcher_ids={this_play.pitcher.player_id}"
for x in used_pitchers:
used_player_ids.append(x.player_id)
id_string += f"&used_pitcher_ids={x.player_id}"
logger.info(f"used ids: {used_player_ids}")
all_links = get_game_cardset_links(session, this_play.game)
if len(all_links) > 0:
cardset_string = ""
for x in all_links:
cardset_string += f"&cardset_id={x.cardset_id}"
else:
cardset_string = ""
logger.info(f"cardset_string: {cardset_string}")
rp_json = await db_get(
f"teams/{ai_team.id}/rp/{this_play.game.game_type.split('-run')[0]}?need={need}{id_string}{cardset_string}"
)
rp_player = await get_player_or_none(
session, player_id=get_player_id_from_dict(rp_json)
)
if rp_player is None:
log_exception(
PlayerNotFoundException, f"Reliever not found for the {ai_team.lname}"
)
logger.info(f"rp_player: {rp_player}")
rp_card = await get_or_create_ai_card(session, rp_player, ai_team)
logger.info(f"rp_card: {rp_card}")
return rp_card
def substitute_player(
session, this_play: Play, old_player: Lineup, new_player: Card, position: str
) -> Lineup:
logger.info(
f"Substituting {new_player.player.name_with_desc} in for {old_player.card.player.name_with_desc} at {position}"
)
new_lineup = Lineup(
team=old_player.team,
player=new_player.player,
card=new_player,
position=position,
batting_order=old_player.batting_order,
game=this_play.game,
after_play=max(this_play.play_num - 1, 0),
replacing_id=old_player.id,
)
logger.info(f"new_lineup: {new_lineup}")
session.add(new_lineup)
logger.info(f"De-activating last player")
old_player.active = False
session.add(old_player)
logger.info(f"Updating play's pitcher")
this_play.pitcher = new_lineup
session.add(this_play)
session.commit()
session.refresh(new_lineup)
return new_lineup