Adds idempotency guard to prevent race conditions when multiple users submit commands for the same play simultaneously. Changes: - Add PlayLockedException for locked play detection - Implement lock check in checks_log_interaction() - Acquire lock (play.locked = True) before processing commands - Release lock (play.locked = False) after play completion - Add warning logs for rejected duplicate submissions - Add /diagnostics endpoint to health server for debugging This prevents database corruption and duplicate processing when users spam commands like "log xcheck" while the first is still processing. Tested successfully in Discord - duplicate commands now properly return PlayLockedException with instructions to wait. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
5141 lines
183 KiB
Python
5141 lines
183 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 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":
|
|
await get_position(session, batter.card, batter.position)
|
|
|
|
return this_game.initialize_play(session)
|
|
|
|
|
|
def get_obc(on_first=None, on_second=None, on_third=None) -> int:
|
|
if on_third is not None:
|
|
if on_second is not None:
|
|
if on_first is not None:
|
|
obc = 7
|
|
else:
|
|
obc = 6
|
|
elif on_first is not None:
|
|
obc = 5
|
|
else:
|
|
obc = 3
|
|
elif on_second is not None:
|
|
if on_first is not None:
|
|
obc = 4
|
|
else:
|
|
obc = 2
|
|
elif on_first is not None:
|
|
obc = 1
|
|
else:
|
|
obc = 0
|
|
|
|
return obc
|
|
|
|
|
|
def get_re24(
|
|
this_play: Play, runs_scored: int, new_obc: int, new_starting_outs: int
|
|
) -> float:
|
|
re_data = {
|
|
0: [0.457, 0.231, 0.077],
|
|
1: [0.793, 0.438, 0.171],
|
|
2: [1.064, 0.596, 0.259],
|
|
4: [1.373, 0.772, 0.351],
|
|
3: [1.340, 0.874, 0.287],
|
|
5: [1.687, 1.042, 0.406],
|
|
6: [1.973, 1.311, 0.448],
|
|
7: [2.295, 1.440, 0.618],
|
|
}
|
|
|
|
start_re24 = re_data[this_play.on_base_code][this_play.starting_outs]
|
|
end_re24 = (
|
|
0
|
|
if this_play.starting_outs + this_play.outs > 2
|
|
else re_data[new_obc][new_starting_outs]
|
|
)
|
|
return round(end_re24 - start_re24 + runs_scored, 3)
|
|
|
|
|
|
def get_wpa(this_play: Play, next_play: Play):
|
|
"""
|
|
Returns wpa relative to batting team of this_play. Negative value if bad play, positive value if good play.
|
|
"""
|
|
new_rd = next_play.home_score - next_play.away_score
|
|
if new_rd > 6:
|
|
new_rd = 6
|
|
elif new_rd < -6:
|
|
new_rd = -6
|
|
|
|
old_rd = this_play.home_score - this_play.away_score
|
|
if old_rd > 6:
|
|
old_rd = 6
|
|
elif old_rd < -6:
|
|
old_rd = -6
|
|
|
|
# print(f'get_wpa: new_rd = {new_rd} / old_rd = {old_rd}')
|
|
if (
|
|
next_play.inning_num >= 9 and new_rd > 0 and next_play.inning_half == "bot"
|
|
) or (next_play.inning_num > 9 and new_rd > 0 and next_play.is_new_inning):
|
|
# print(f'manually setting new_win_ex to 1.0')
|
|
new_win_ex = 1.0
|
|
else:
|
|
inning_num = 9 if next_play.inning_num > 9 else next_play.inning_num
|
|
new_win_ex = 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."
|
|
)
|
|
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
|
|
) -> tuple[Game, Team, Play]:
|
|
"""
|
|
Commits this_play
|
|
"""
|
|
logger.info(
|
|
f"log interaction checks for {interaction.user.name} in channel {interaction.channel.name}"
|
|
)
|
|
await interaction.response.defer(thinking=True)
|
|
this_game = get_channel_game_or_none(session, interaction.channel_id)
|
|
if this_game is None:
|
|
raise GameNotFoundException("I don't see an active game in this channel.")
|
|
|
|
owner_team = await get_team_or_none(session, gm_id=interaction.user.id)
|
|
if owner_team is None:
|
|
logger.exception(
|
|
f"{command_name} command: No team found for GM ID {interaction.user.id}"
|
|
)
|
|
raise TeamNotFoundException(f"Do I know you? I cannot find your team.")
|
|
|
|
if "gauntlet" in this_game.game_type:
|
|
gauntlet_abbrev = f"Gauntlet-{owner_team.abbrev}"
|
|
owner_team = await get_team_or_none(session, team_abbrev=gauntlet_abbrev)
|
|
if owner_team is None:
|
|
logger.exception(
|
|
f"{command_name} command: No gauntlet team found with abbrev {gauntlet_abbrev}"
|
|
)
|
|
raise TeamNotFoundException(
|
|
f"Hm, I was not able to find a gauntlet team for you."
|
|
)
|
|
|
|
if not owner_team.id in [this_game.away_team_id, this_game.home_team_id]:
|
|
if interaction.user.id != 258104532423147520:
|
|
logger.exception(
|
|
f"{interaction.user.display_name} tried to run a command in Game {this_game.id} when they aren't a GM in the game."
|
|
)
|
|
raise TeamNotFoundException(
|
|
"Bruh. Only GMs of the active teams can log plays."
|
|
)
|
|
else:
|
|
await interaction.channel.send(
|
|
f"Cal is bypassing the GM check to run the {command_name} command"
|
|
)
|
|
|
|
this_play = this_game.current_play_or_none(session)
|
|
if this_play is None:
|
|
logger.error(
|
|
f"{command_name} command: No play found for Game ID {this_game.id} - attempting to initialize play"
|
|
)
|
|
this_play = activate_last_play(session, this_game)
|
|
|
|
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, wait 30 seconds and try again (auto-unlock will trigger)."
|
|
)
|
|
|
|
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
|