New Position exception Pull scouting data with lineups More bunt types String validation on gameplay models AI Defensive alignment
1206 lines
49 KiB
Python
1206 lines
49 KiB
Python
import datetime
|
|
import logging
|
|
from typing import Literal
|
|
|
|
import discord
|
|
import pydantic
|
|
|
|
from pydantic import field_validator
|
|
from sqlmodel import Session, SQLModel, UniqueConstraint, create_engine, select, or_, Field, Relationship, text
|
|
from sqlalchemy import func, desc
|
|
|
|
from exceptions import *
|
|
from in_game.managerai_responses import DefenseResponse, JumpResponse, TagResponse, ThrowResponse, UncappedRunResponse
|
|
|
|
|
|
logger = logging.getLogger('discord_app')
|
|
sqlite_url = 'sqlite:///storage/gameplay.db'
|
|
connect_args = {"check_same_thread": False}
|
|
engine = create_engine(sqlite_url, echo=False, connect_args=connect_args)
|
|
CACHE_LIMIT = 1209600 # in seconds
|
|
SBA_COLOR = 'a6ce39'
|
|
SBA_LOGO = 'https://sombaseball.ddns.net/static/images/sba-logo.png'
|
|
|
|
|
|
class ManagerAiBase(SQLModel):
|
|
id: int | None = Field(primary_key=True)
|
|
name: str = Field(index=True)
|
|
steal: int | None = Field(default=5)
|
|
running: int | None = Field(default=5)
|
|
hold: int | None = Field(default=5)
|
|
catcher_throw: int | None = Field(default=5)
|
|
uncapped_home: int | None = Field(default=5)
|
|
uncapped_third: int | None = Field(default=5)
|
|
uncapped_trail: int | None = Field(default=5)
|
|
bullpen_matchup: int | None = Field(default=5)
|
|
behind_aggression: int | None = Field(default=5)
|
|
ahead_aggression: int | None = Field(default=5)
|
|
decide_throw: int | None = Field(default=5)
|
|
|
|
|
|
class GameCardsetLink(SQLModel, table=True):
|
|
game_id: int | None = Field(default=None, foreign_key='game.id', primary_key=True)
|
|
cardset_id: int | None = Field(default=None, foreign_key='cardset.id', primary_key=True)
|
|
priority: int | None = Field(default=1, index=True)
|
|
|
|
game: 'Game' = Relationship(back_populates='cardset_links')
|
|
cardset: 'Cardset' = Relationship(back_populates='game_links')
|
|
|
|
|
|
class TeamBase(SQLModel):
|
|
id: int = Field(primary_key=True)
|
|
abbrev: str = Field(index=True)
|
|
sname: str
|
|
lname: str
|
|
gmid: int = Field(index=True)
|
|
gmname: str
|
|
gsheet: str
|
|
wallet: int
|
|
team_value: int
|
|
collection_value: int
|
|
logo: str | None = Field(default=None)
|
|
color: str
|
|
season: int
|
|
career: int
|
|
ranking: int
|
|
has_guide: bool
|
|
is_ai: bool
|
|
created: datetime.datetime = Field(default_factory=datetime.datetime.now, nullable=True)
|
|
|
|
@property
|
|
def description(self) -> str:
|
|
return f'{self.id}. {self.abbrev} {self.lname}, {"is_ai" if self.is_ai else "human"}'
|
|
|
|
|
|
class Team(TeamBase, table=True):
|
|
cards: list['Card'] = Relationship(back_populates='team', cascade_delete=True)
|
|
lineups: list['Lineup'] = Relationship(back_populates='team', cascade_delete=True)
|
|
# away_games: list['Game'] = Relationship(back_populates='away_team')
|
|
# home_games: list['Game'] = Relationship(back_populates='home_team')
|
|
|
|
@property
|
|
def embed(self) -> discord.Embed:
|
|
embed = discord.Embed(
|
|
title=f'{self.lname}',
|
|
color=int(self.color, 16) if self.color else int(SBA_COLOR, 16)
|
|
)
|
|
embed.set_footer(text=f'Paper Dynasty Season {self.season}', icon_url=SBA_LOGO)
|
|
embed.set_thumbnail(url=self.logo if self.logo else SBA_LOGO)
|
|
return embed
|
|
|
|
|
|
class Game(SQLModel, table=True):
|
|
id: int | None = Field(default=None, primary_key=True)
|
|
away_team_id: int = Field(foreign_key='team.id')
|
|
home_team_id: int = Field(foreign_key='team.id')
|
|
channel_id: int = Field(index=True)
|
|
season: int
|
|
active: bool | None = Field(default=True)
|
|
is_pd: bool | None = Field(default=True)
|
|
ranked: bool | None = Field(default=False)
|
|
short_game: bool | None = Field(default=False)
|
|
week: int | None = Field(default=None)
|
|
game_num: int | None = Field(default=None)
|
|
away_roster_id: int | None = Field(default=None)
|
|
home_roster_id: int | None = Field(default=None)
|
|
first_message: str | None = Field(default=None)
|
|
ai_team: str | None = Field(default=None)
|
|
game_type: str
|
|
|
|
cardset_links: list[GameCardsetLink] = Relationship(back_populates='game', cascade_delete=True)
|
|
away_team: Team = Relationship(
|
|
# back_populates='away_games',
|
|
# sa_relationship_kwargs={
|
|
# 'primaryjoin': 'Game.away_team_id==Team.id',
|
|
# 'foreign_keys': '[Game.away_team.id]',
|
|
# 'lazy': 'joined'
|
|
# }
|
|
sa_relationship_kwargs=dict(foreign_keys="[Game.away_team_id]")
|
|
)
|
|
home_team: Team = Relationship(
|
|
# back_populates='home_games',
|
|
# sa_relationship_kwargs={
|
|
# 'primaryjoin': 'Game.home_team_id==Team.id',
|
|
# 'foreign_keys': '[Game.home_team.id]',
|
|
# 'lazy': 'joined'
|
|
# }
|
|
sa_relationship_kwargs=dict(foreign_keys="[Game.home_team_id]")
|
|
)
|
|
lineups: list['Lineup'] = Relationship(back_populates='game', cascade_delete=True)
|
|
plays: list['Play'] = Relationship(back_populates='game', cascade_delete=True)
|
|
|
|
@field_validator('ai_team', 'game_type')
|
|
def lowercase_strings(cls, value: str) -> str:
|
|
return value.lower()
|
|
|
|
@property
|
|
def cardset_param_string(self) -> str:
|
|
pri_cardsets = ''
|
|
back_cardsets = ''
|
|
for link in self.cardset_links:
|
|
if link.priority == 1:
|
|
pri_cardsets += f'&cardset_id={link.cardset_id}'
|
|
else:
|
|
back_cardsets += f'&backup_cardset_id={link.cardset_id}'
|
|
return f'{pri_cardsets}{back_cardsets}'
|
|
|
|
def current_play_or_none(self, session: Session):
|
|
this_play = session.exec(select(Play).where(Play.game == self, Play.complete == False).order_by(Play.id.desc()).limit(1)).all()
|
|
if len(this_play) == 1:
|
|
return this_play[0]
|
|
else:
|
|
return None
|
|
|
|
def get_scorebug_embed(self, session: Session, full_length: bool = True, classic: bool = True) -> discord.Embed:
|
|
gt_string = ' - Unlimited'
|
|
if self.game_type == 'minor-league':
|
|
gt_string = ' - Minor League'
|
|
elif self.game_type == 'major-league':
|
|
gt_string = ' - Major League'
|
|
elif self.game_type == 'hall-of-fame':
|
|
gt_string = ' - Hall of Fame'
|
|
elif 'gauntlet' in self.game_type:
|
|
gt_string = ' - Gauntlet'
|
|
elif 'flashback' in self.game_type:
|
|
gt_string = ' - Flashback'
|
|
elif 'exhibition' in self.game_type:
|
|
gt_string = ' - Exhibition'
|
|
logger.info(f'gameplay_models - Game.get_scorebug_embed - this_game: {self} / gt_string: {gt_string}')
|
|
|
|
embed = discord.Embed(
|
|
title=f'{self.away_team.sname} @ {self.home_team.sname}{gt_string}',
|
|
color=int(SBA_COLOR, 16)
|
|
)
|
|
|
|
curr_play = self.current_play_or_none(session)
|
|
|
|
if curr_play is None:
|
|
try:
|
|
curr_play = self.initialize_play(session)
|
|
except LineupsMissingException as e:
|
|
logger.debug(f'gameplay_models - Game.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
|
|
)
|
|
|
|
if classic:
|
|
embed.add_field(
|
|
name='Pitcher',
|
|
value=curr_play.pitcher.player.name_card_link('pitching')
|
|
)
|
|
embed.add_field(
|
|
name='Batter',
|
|
value=curr_play.batter.player.name_card_link('batting')
|
|
)
|
|
|
|
baserunner_string = ''
|
|
if curr_play.on_first is not None:
|
|
baserunner_string += f'On First: {curr_play.on_first.player.name_card_link('batting')}\n'
|
|
if curr_play.on_second is not None:
|
|
baserunner_string += f'On Second: {curr_play.on_second.player.name_card_link('batting')}\n'
|
|
if curr_play.on_third is not None:
|
|
baserunner_string += f'On Third: {curr_play.on_third.player.name_card_link('batting')}'
|
|
logger.info(f'gameplay_models - Game.get_scorebug_embed - baserunner_string: {baserunner_string}')
|
|
|
|
if len(baserunner_string) > 0:
|
|
embed.add_field(name=' ', value=' ', inline=False)
|
|
embed.add_field(name='Baserunners', value=baserunner_string)
|
|
embed.add_field(name='Catcher', value=curr_play.catcher.player.name_card_link('batter'))
|
|
|
|
ai_note = curr_play.ai_note
|
|
logger.info(f'gameplay_models - Game.get_scorebug_embed - ai_note: {ai_note}')
|
|
if len(ai_note) > 0:
|
|
gm_name = self.home_team.gmname if self.ai_team == 'home' else self.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'{self.away_team.abbrev} Lineup',
|
|
value=self.team_lineup(session, self.away_team)
|
|
)
|
|
embed.add_field(
|
|
name=f'{self.home_team.abbrev} Lineup',
|
|
value=self.team_lineup(session, self.home_team)
|
|
)
|
|
else:
|
|
embed.add_field(
|
|
name=f'{self.away_team.abbrev} Lineup',
|
|
value=self.team_lineup(session, self.away_team)
|
|
)
|
|
embed.add_field(
|
|
name=f'{self.home_team.abbrev} Lineup',
|
|
value=self.team_lineup(session, self.home_team)
|
|
)
|
|
|
|
return embed
|
|
|
|
def initialize_play(self, session: Session):
|
|
"""
|
|
Commits new_play
|
|
"""
|
|
existing_play = self.current_play_or_none(session)
|
|
if existing_play is not None:
|
|
return existing_play
|
|
|
|
all_plays = session.exec(select(func.count(Play.id)).where(Play.game == self)).one()
|
|
if all_plays > 0:
|
|
raise PlayInitException(f'{all_plays} plays for game {self.id} already exist, but all are complete.')
|
|
|
|
leadoff_batter, home_pitcher, home_catcher = None, None, None
|
|
home_positions, away_positions = [], []
|
|
for line in [x for x in self.lineups if x.active]:
|
|
if line.team == self.away_team:
|
|
if line.position not in away_positions:
|
|
away_positions.append(line.position)
|
|
if line.batting_order == 1:
|
|
leadoff_batter = line
|
|
else:
|
|
if line.position not in home_positions:
|
|
home_positions.append(line.position)
|
|
if line.position == 'P':
|
|
home_pitcher = line
|
|
elif line.position == 'C':
|
|
home_catcher = line
|
|
|
|
if len(home_positions) != 10:
|
|
e_msg = f'Only {len(home_positions)} players found on home team'
|
|
log_exception(LineupsMissingException, e_msg)
|
|
if len(away_positions) != 10:
|
|
e_msg = f'Only {len(away_positions)} players found on away team'
|
|
log_exception(LineupsMissingException, e_msg)
|
|
if None in [leadoff_batter, home_pitcher, home_catcher]:
|
|
e_msg = f'Could not set the initial pitcher, catcher, and batter'
|
|
log_exception(LineupsMissingException, e_msg)
|
|
|
|
manager_ai_id = ((datetime.datetime.now().day * (self.away_team_id if self.ai_team == 'away' else self.home_team_id)) % 3) + 1
|
|
if manager_ai_id > 3 or manager_ai_id < 1:
|
|
manager_ai_id = 1
|
|
|
|
new_play = Play(
|
|
game=self,
|
|
play_num=1,
|
|
batter=leadoff_batter,
|
|
pitcher=home_pitcher,
|
|
batter_pos=leadoff_batter.position,
|
|
catcher=home_catcher,
|
|
is_tied=True,
|
|
is_new_inning=True,
|
|
managerai_id=manager_ai_id
|
|
)
|
|
session.add(new_play)
|
|
session.commit()
|
|
session.refresh(new_play)
|
|
|
|
new_play.init_ai(session)
|
|
|
|
return new_play
|
|
|
|
def team_lineup(self, session: Session, team: Team) -> str:
|
|
all_lineups = session.exec(select(Lineup).where(Lineup.team == team, Lineup.game == self).order_by(Lineup.batting_order)).all()
|
|
|
|
lineup_val = ''
|
|
for line in all_lineups:
|
|
lineup_val += f'{line.batting_order}. {line.player.name_card_link("batting" if line.position != "P" else "pitching")} {line.position}\n'
|
|
|
|
return lineup_val
|
|
|
|
|
|
class ManagerAi(ManagerAiBase, table=True):
|
|
plays: list['Play'] = Relationship(back_populates='managerai')
|
|
def create_ai(session: Session = None):
|
|
def get_new_ai(this_session: Session):
|
|
all_ai = this_session.exec(select(ManagerAi.id)).all()
|
|
if len(all_ai) == 0:
|
|
logger.info(f'Creating ManagerAI records')
|
|
new_ai = [
|
|
ManagerAi(
|
|
name='Balanced'
|
|
),
|
|
ManagerAi(
|
|
name='Yolo',
|
|
steal=10,
|
|
running=10,
|
|
hold=5,
|
|
catcher_throw=10,
|
|
uncapped_home=10,
|
|
uncapped_third=10,
|
|
uncapped_trail=10,
|
|
bullpen_matchup=3,
|
|
behind_aggression=10,
|
|
ahead_aggression=10,
|
|
decide_throw=10
|
|
),
|
|
ManagerAi(
|
|
name='Safe',
|
|
steal=3,
|
|
running=3,
|
|
hold=8,
|
|
catcher_throw=5,
|
|
uncapped_home=5,
|
|
uncapped_third=3,
|
|
uncapped_trail=5,
|
|
bullpen_matchup=8,
|
|
behind_aggression=5,
|
|
ahead_aggression=1,
|
|
decide_throw=1
|
|
)
|
|
]
|
|
for x in new_ai:
|
|
session.add(x)
|
|
session.commit()
|
|
|
|
if session is None:
|
|
with Session(engine) as session:
|
|
get_new_ai(session)
|
|
else:
|
|
get_new_ai(session)
|
|
|
|
return True
|
|
|
|
def check_jump(self, session: Session, this_game: Game, to_base: Literal[2, 3, 4]) -> JumpResponse | None:
|
|
this_resp = JumpResponse()
|
|
this_play = this_game.current_play_or_none(session)
|
|
if this_play is None:
|
|
raise GameException(f'No game found while checking for jump')
|
|
|
|
num_outs = this_play.starting_outs
|
|
run_diff = this_play.away_score - this_play.home_score
|
|
if this_game.ai_team == 'home':
|
|
run_diff = run_diff * -1
|
|
|
|
if to_base == 2:
|
|
match self.steal:
|
|
case 10:
|
|
this_resp.min_safe = 12 + num_outs
|
|
case self.steal if self.steal > 8 and run_diff <= 5:
|
|
this_resp.min_safe = 13 + num_outs
|
|
case self.steal if self.steal > 6 and run_diff <= 5:
|
|
this_resp.min_safe = 14 + num_outs
|
|
case self.steal if self.steal > 4 and num_outs < 2 and run_diff <= 5:
|
|
this_resp.min_safe = 15 + num_outs
|
|
case self.steal if self.steal > 2 and num_outs < 2 and run_diff <= 5:
|
|
this_resp.min_safe = 16 + num_outs
|
|
case _:
|
|
this_resp = 17 + num_outs
|
|
|
|
if self.steal > 7 and num_outs < 2 and run_diff <= 5:
|
|
this_resp.run_if_auto_jump = True
|
|
elif self.steal < 5:
|
|
this_resp.must_auto_jump = True
|
|
|
|
elif to_base == 3:
|
|
match self.steal:
|
|
case 10:
|
|
this_resp.min_safe = 12 + num_outs
|
|
case self.steal if self.steal > 6 and num_outs < 2 and run_diff <= 5:
|
|
this_resp.min_safe = 15 + num_outs
|
|
case _:
|
|
this_resp.min_safe = None
|
|
|
|
if self.steal == 10 and num_outs < 2 and run_diff <= 5:
|
|
this_resp.run_if_auto_jump = True
|
|
elif self.steal <= 5:
|
|
this_resp.must_auto_jump = True
|
|
|
|
elif run_diff in [-1, 0]:
|
|
if self.steal == 10:
|
|
this_resp.min_safe = 5
|
|
elif self.steal > 5:
|
|
this_resp.min_safe = 7
|
|
elif this_play.inning_num > 7 and self.steal >= 5:
|
|
this_resp.min_safe = 6
|
|
|
|
return this_resp
|
|
|
|
def tag_from_second(self, session: Session, this_game: Game) -> TagResponse:
|
|
this_resp = TagResponse()
|
|
this_play = this_game.current_play_or_none(session)
|
|
if this_play is None:
|
|
raise GameException(f'No game found while checking tag_from_second')
|
|
|
|
ai_rd = this_play.ai_run_diff()
|
|
aggression_mod = abs(self.ahead_aggression - 5 if ai_rd > 0 else self.behind_aggression - 5)
|
|
adjusted_running = self.running + aggression_mod
|
|
|
|
if adjusted_running >= 8:
|
|
this_resp.min_safe = 4
|
|
elif adjusted_running >= 5:
|
|
this_resp.min_safe = 7
|
|
else:
|
|
this_resp.min_safe = 10
|
|
|
|
if this_play.starting_outs == 1:
|
|
this_resp.min_safe -= 2
|
|
else:
|
|
this_resp.min_safe += 2
|
|
|
|
return this_resp
|
|
|
|
def throw_at_uncapped(self, session: Session, this_game: Game) -> ThrowResponse:
|
|
this_resp = ThrowResponse()
|
|
this_play = this_game.current_play_or_none(session)
|
|
if this_play is None:
|
|
raise GameException(f'No game found while checking throw_at_uncapped')
|
|
|
|
ai_rd = this_play.ai_run_diff()
|
|
aggression = self.ahead_aggression if ai_rd > 0 else self.behind_aggression
|
|
current_outs = this_play.starting_outs + this_play.outs
|
|
|
|
if ai_rd > 5:
|
|
if self.ahead_aggression > 5:
|
|
this_resp.at_trail_runner = True
|
|
this_resp.trail_max_safe_delta = -4 + current_outs
|
|
else:
|
|
this_resp.cutoff = True
|
|
elif ai_rd > 2:
|
|
if self.ahead_aggression > 8:
|
|
this_resp.at_trail_runner = True
|
|
this_resp.trail_max_safe_delta = -4 + current_outs
|
|
elif ai_rd > 0:
|
|
if self.ahead_aggression > 8:
|
|
this_resp.at_trail_runner = True
|
|
this_resp.trail_max_safe_delta = -6 + current_outs
|
|
elif ai_rd > -3:
|
|
if self.behind_aggression < 5:
|
|
this_resp.at_trail_runner = True
|
|
this_resp.trail_max_safe_delta = -6 + current_outs
|
|
elif ai_rd > -6:
|
|
if self.behind_aggression < 5:
|
|
this_resp.at_trail_runner = True
|
|
this_resp.trail_max_safe_delta = -4 + current_outs
|
|
else:
|
|
if self.behind_aggression < 5:
|
|
this_resp.at_trail_runner = True
|
|
this_resp.trail_max_safe_delta = -4
|
|
|
|
return this_resp
|
|
|
|
def uncapped_advance(self, session: Session, this_game: Game, lead_base: int, trail_base: int) -> UncappedRunResponse:
|
|
this_resp = UncappedRunResponse()
|
|
this_play = this_game.current_play_or_none(session)
|
|
if this_play is None:
|
|
raise GameException(f'No game found while checking uncapped_advance_lead')
|
|
|
|
ai_rd = this_play.ai_run_diff()
|
|
aggression = self.ahead_aggression - 5 if ai_rd > 0 else self.behind_aggression - 5
|
|
|
|
if ai_rd > 4:
|
|
if lead_base == 4:
|
|
this_resp.min_safe = 16 - this_play.starting_outs - aggression
|
|
this_resp.send_trail = True
|
|
this_resp.trail_min_safe = 10 - aggression - this_play.starting_outs - this_play.outs
|
|
elif lead_base == 3:
|
|
this_resp.min_safe = 14 + (this_play.starting_outs * 2) - aggression
|
|
if this_play.starting_outs + this_play.outs >= 2:
|
|
this_resp.send_trail = False
|
|
elif ai_rd > 1 or ai_rd < -2:
|
|
if lead_base == 4:
|
|
this_resp.min_safe = 12 - this_play.starting_outs - aggression
|
|
this_resp.send_trail = True
|
|
this_resp.trail_min_safe = 10 - aggression - this_play.starting_outs - this_play.outs
|
|
elif lead_base == 3:
|
|
this_resp.min_safe = 12 + (this_play.starting_outs * 2) - (aggression * 2)
|
|
if this_play.starting_outs + this_play.outs >= 2:
|
|
this_resp.send_trail = False
|
|
else:
|
|
if lead_base == 4:
|
|
this_resp.min_safe = 10 - this_play.starting_outs - aggression
|
|
this_resp.send_trail = True
|
|
this_resp.trail_min_safe = 2
|
|
elif lead_base == 3:
|
|
this_resp.min_safe = 14 + (this_play.starting_outs * 2) - aggression
|
|
if this_play.starting_outs + this_play.outs >= 2:
|
|
this_resp.send_trail = False
|
|
|
|
if this_resp.min_safe > 20:
|
|
this_resp.min_safe = 20
|
|
if this_resp.min_safe < 1:
|
|
this_resp.min_safe = 1
|
|
if this_resp.trail_min_safe > 20:
|
|
this_resp.min_safe = 20
|
|
if this_resp.trail_min_safe < 1:
|
|
this_resp.min_safe = 1
|
|
|
|
return this_resp
|
|
|
|
def defense_alignment(self, session: Session, this_game: Game) -> DefenseResponse:
|
|
this_resp = DefenseResponse()
|
|
this_play = this_game.current_play_or_none(session)
|
|
if this_play is None:
|
|
raise GameException(f'No game found while checking uncapped_advance_lead')
|
|
|
|
ai_rd = this_play.ai_run_diff()
|
|
aggression = self.ahead_aggression - 5 if ai_rd > 0 else self.behind_aggression - 5
|
|
|
|
if self.starting_outs == 2 and self.on_base_code > 0:
|
|
if self.on_base_code == 1:
|
|
this_resp.hold_first = True
|
|
elif self.on_base_code == 2:
|
|
this_resp.hold_second = True
|
|
elif self.on_base_code in [4, 5, 7]:
|
|
this_resp.hold_first = True
|
|
this_resp.hold_second = True
|
|
# elif self.on_base_code == 5:
|
|
# ai_note += f'- hold the runner on first\n'
|
|
elif self.on_base_code == 6:
|
|
ai_note += f'- hold {self.on_second.player.name} on 2nd\n'
|
|
elif self.on_base_code in [1, 5]:
|
|
runner = self.on_first.player
|
|
if self.on_first.card.batterscouting.battingcard.steal_auto:
|
|
ai_note += f'- hold {runner.name} on 1st\n'
|
|
elif self.on_base_code in [2, 4]:
|
|
if self.on_second.card.batterscouting.battingcard.steal_low + max(self.pitcher.card.pitcherscouting.pitchingcard.hold, 5) >= 14:
|
|
ai_note += f'- hold {self.on_second.player.name} on 2nd\n'
|
|
|
|
# Defensive Alignment
|
|
if self.on_third and self.starting_outs < 2:
|
|
if self.could_walkoff:
|
|
ai_note += f'- play the outfield and infield in'
|
|
elif abs(self.away_score - self.home_score) <= 3:
|
|
ai_note += f'- play the whole infield in\n'
|
|
else:
|
|
ai_note += f'- play the corners in\n'
|
|
|
|
if len(ai_note) == 0 and self.on_base_code > 0:
|
|
ai_note += f'- play straight up\n'
|
|
|
|
|
|
class CardsetBase(SQLModel):
|
|
id: int | None = Field(default=None, primary_key=True)
|
|
name: str
|
|
ranked_legal: bool | None = Field(default=False)
|
|
|
|
|
|
class Cardset(CardsetBase, table=True):
|
|
game_links: list[GameCardsetLink] = Relationship(back_populates='cardset', cascade_delete=True)
|
|
players: list['Player'] = Relationship(back_populates='cardset')
|
|
|
|
|
|
class PlayerBase(SQLModel):
|
|
id: int | None = Field(primary_key=True)
|
|
name: str
|
|
cost: int
|
|
image: str
|
|
mlbclub: str
|
|
franchise: str
|
|
cardset_id: int | None = Field(default=None, foreign_key='cardset.id')
|
|
set_num: int
|
|
rarity_id: int | None = Field(default=None)
|
|
pos_1: str
|
|
description: str
|
|
quantity: int | None = Field(default=999)
|
|
image2: str | None = Field(default=None)
|
|
pos_2: str | None = Field(default=None)
|
|
pos_3: str | None = Field(default=None)
|
|
pos_4: str | None = Field(default=None)
|
|
pos_5: str | None = Field(default=None)
|
|
pos_6: str | None = Field(default=None)
|
|
pos_7: str | None = Field(default=None)
|
|
pos_8: str | None = Field(default=None)
|
|
headshot: str | None = Field(default=None)
|
|
vanity_card: str | None = Field(default=None)
|
|
strat_code: str | None = Field(default=None)
|
|
bbref_id: str | None = Field(default=None)
|
|
fangr_id: str | None = Field(default=None)
|
|
mlbplayer_id: int | None = Field(default=None)
|
|
created: datetime.datetime = Field(default_factory=datetime.datetime.now, nullable=True)
|
|
|
|
@field_validator('pos_1', 'pos_2', 'pos_3', 'pos_4', 'pos_5', 'pos_6', 'pos_7', 'pos_8')
|
|
def uppercase_strings(cls, value: str) -> str:
|
|
if value is not None:
|
|
return value.upper()
|
|
else:
|
|
return value
|
|
|
|
@property
|
|
def p_card_url(self):
|
|
if 'pitching' in self.image:
|
|
return self.image
|
|
elif self.image2 is not None and 'pitching' in self.image2:
|
|
return self.image2
|
|
else:
|
|
logger.error(f'gameplay_models - PlayerBase - pitching card url not found for {self.id}. {self.description} {self.name}')
|
|
return self.image
|
|
|
|
@property
|
|
def b_card_url(self):
|
|
if 'batting' in self.image:
|
|
return self.image
|
|
elif self.image2 is not None and 'batting' in self.image2:
|
|
return self.image2
|
|
else:
|
|
logger.error(f'gameplay_models - PlayerBase - batting card url not found for {self.id}. {self.description} {self.name}')
|
|
return self.image
|
|
|
|
def name_card_link(self, which: Literal['pitching', 'batting']):
|
|
if which == 'pitching':
|
|
return f'[{self.name}]({self.p_card_url})'
|
|
else:
|
|
return f'[{self.name}]({self.b_card_url})'
|
|
|
|
|
|
class Player(PlayerBase, table=True):
|
|
cardset: Cardset = Relationship(back_populates='players')
|
|
cards: list['Card'] = Relationship(back_populates='player', cascade_delete=True)
|
|
lineups: list['Lineup'] = Relationship(back_populates='player', cascade_delete=True)
|
|
positions: list['PositionRating'] = Relationship(back_populates='player', cascade_delete=True)
|
|
|
|
@property
|
|
def name_with_desc(self):
|
|
return f'{self.description} {self.name}'
|
|
|
|
|
|
def player_description(player: Player = None, player_dict: dict = None) -> str:
|
|
if player is None and player_dict is None:
|
|
err = 'One of "player" or "player_dict" must be included to get full description'
|
|
logger.error(f'gameplay_models - player_description - {err}')
|
|
raise TypeError(err)
|
|
|
|
if player is not None:
|
|
return f'{player.description} {player.name}'
|
|
|
|
r_val = f'{player_dict['description']}'
|
|
if 'name' in player_dict:
|
|
r_val += f' {player_dict["name"]}'
|
|
elif 'p_name' in player_dict:
|
|
r_val += f' {player_dict["p_name"]}'
|
|
return r_val
|
|
|
|
|
|
class BattingCardBase(SQLModel):
|
|
id: int | None = Field(default=None, primary_key=True)
|
|
variant: int | None = Field(default=0)
|
|
steal_low: int = Field(default=0, ge=0, le=20)
|
|
steal_high: int = Field(default=0, ge=0, le=20)
|
|
steal_auto: bool = Field(default=False)
|
|
steal_jump: float = Field(default=0.0, ge=0.0, le=1.0)
|
|
bunting: str = Field(default='C')
|
|
hit_and_run: str = Field(default='C')
|
|
running: int = Field(default=10, ge=1, le=20)
|
|
offense_col: int = Field(ge=1, le=3)
|
|
hand: str
|
|
created: datetime.datetime = Field(default_factory=datetime.datetime.now, nullable=True)
|
|
# created: datetime.datetime | None = Field(sa_column_kwargs={"server_default": text("CURRENT_TIMESTAMP"),})
|
|
|
|
@field_validator('hand')
|
|
def lowercase_hand(cls, value: str) -> str:
|
|
return value.lower()
|
|
|
|
|
|
class BattingCard(BattingCardBase, table=True):
|
|
pass
|
|
|
|
|
|
class BattingRatingsBase(SQLModel):
|
|
id: int | None = Field(default=None, primary_key=True)
|
|
homerun: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
bp_homerun: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
triple: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
double_three: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
double_two: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
double_pull: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
single_two: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
single_one: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
single_center: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
bp_single: float = Field(default=0.0, ge=0.0, le=10.0)
|
|
hbp: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
walk: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
strikeout: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
lineout: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
popout: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
flyout_a: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
flyout_bq: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
flyout_lf_b: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
flyout_rf_b: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
groundout_a: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
groundout_b: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
groundout_c: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
avg: float = Field(default=0.0, ge=0.0, le=1.0)
|
|
obp: float = Field(default=0.0, ge=0.0, le=1.0)
|
|
slg: float = Field(default=0.0, ge=0.0, le=4.0)
|
|
pull_rate: float = Field(default=0.0, ge=0.0, le=1.0)
|
|
center_rate: float = Field(default=0.0, ge=0.0, le=1.0)
|
|
slap_rate: float = Field(default=0.0, ge=0.0, le=1.0)
|
|
created: datetime.datetime = Field(default_factory=datetime.datetime.now, nullable=True)
|
|
|
|
|
|
class BattingRatings(BattingRatingsBase, table=True):
|
|
pass
|
|
|
|
|
|
class BatterScoutingBase(SQLModel):
|
|
id: int | None = Field(default=None, primary_key=True)
|
|
battingcard_id: int | None = Field(default=None, foreign_key='battingcard.id')
|
|
ratings_vl_id: int | None = Field(default=None, foreign_key='battingratings.id')
|
|
ratings_vr_id: int | None = Field(default=None, foreign_key='battingratings.id')
|
|
created: datetime.datetime = Field(default_factory=datetime.datetime.now, nullable=True)
|
|
|
|
|
|
class BatterScouting(BatterScoutingBase, table=True):
|
|
battingcard: BattingCard = Relationship() #back_populates='batterscouting')
|
|
ratings_vl: BattingRatings = Relationship(
|
|
sa_relationship_kwargs=dict(foreign_keys="[BatterScouting.ratings_vl_id]")
|
|
)
|
|
ratings_vr: BattingRatings = Relationship(
|
|
sa_relationship_kwargs=dict(foreign_keys="[BatterScouting.ratings_vr_id]")
|
|
)
|
|
cards: list['Card'] = Relationship(back_populates='batterscouting', cascade_delete=True)
|
|
|
|
|
|
class PitchingCardBase(SQLModel):
|
|
id: int | None = Field(default=None, primary_key=True)
|
|
variant: int | None = Field(default=0)
|
|
balk: int = Field(default=0, ge=0, le=20)
|
|
wild_pitch: int = Field(default=0, ge=0, le=20)
|
|
hold: int = Field(default=0, ge=-9, le=9)
|
|
starter_rating: int = Field(default=1, ge=1, le=10)
|
|
relief_rating: int = Field(default=1, ge=1, le=10)
|
|
closer_rating: int | None = Field(default=None, ge=0, le=9)
|
|
offense_col: int = Field(ge=1, le=3)
|
|
batting: str = Field(default='#1WR-C')
|
|
hand: str
|
|
created: datetime.datetime = Field(default_factory=datetime.datetime.now, nullable=True)
|
|
|
|
@field_validator('hand')
|
|
def lowercase_hand(cls, value: str) -> str:
|
|
return value.lower()
|
|
|
|
|
|
class PitchingCard(PitchingCardBase, table=True):
|
|
pass
|
|
|
|
|
|
class PitchingRatingsBase(SQLModel):
|
|
id: int | None = Field(default=None, primary_key=True)
|
|
homerun: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
bp_homerun: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
triple: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
double_three: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
double_two: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
double_cf: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
single_two: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
single_one: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
single_center: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
bp_single: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
hbp: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
walk: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
strikeout: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
flyout_lf_b: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
flyout_cf_b: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
flyout_rf_b: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
groundout_a: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
groundout_b: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
xcheck_p: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
xcheck_c: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
xcheck_1b: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
xcheck_2b: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
xcheck_3b: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
xcheck_ss: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
xcheck_lf: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
xcheck_cf: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
xcheck_rf: float = Field(default=0.0, ge=0.0, le=108.0)
|
|
avg: float = Field(default=0.0, ge=0.0, le=1.0)
|
|
obp: float = Field(default=0.0, ge=0.0, le=1.0)
|
|
slg: float = Field(default=0.0, ge=0.0, le=4.0)
|
|
created: datetime.datetime = Field(default_factory=datetime.datetime.now, nullable=True)
|
|
|
|
|
|
class PitchingRatings(PitchingRatingsBase, table=True):
|
|
pass
|
|
|
|
|
|
class PitcherScoutingBase(SQLModel):
|
|
id: int | None = Field(default=None, primary_key=True)
|
|
pitchingcard_id: int | None = Field(default=None, foreign_key='pitchingcard.id',)
|
|
ratings_vl_id: int | None = Field(default=None, foreign_key='pitchingratings.id')
|
|
ratings_vr_id: int | None = Field(default=None, foreign_key='pitchingratings.id')
|
|
created: datetime.datetime = Field(default_factory=datetime.datetime.now, nullable=True)
|
|
|
|
|
|
class PitcherScouting(PitcherScoutingBase, table=True):
|
|
pitchingcard: PitchingCard = Relationship()
|
|
ratings_vl: PitchingRatings = Relationship(
|
|
sa_relationship_kwargs=dict(foreign_keys="[PitcherScouting.ratings_vl_id]")
|
|
)
|
|
ratings_vr: PitchingRatings = Relationship(
|
|
sa_relationship_kwargs=dict(foreign_keys="[PitcherScouting.ratings_vr_id]")
|
|
)
|
|
cards: list['Card'] = Relationship(back_populates='pitcherscouting', cascade_delete=True)
|
|
|
|
|
|
class CardBase(SQLModel):
|
|
id: int | None = Field(default=None, primary_key=True)
|
|
player_id: int = Field(foreign_key='player.id', index=True, ondelete='CASCADE')
|
|
team_id: int = Field(foreign_key='team.id', index=True, ondelete='CASCADE')
|
|
batterscouting_id: int | None = Field(default=None, foreign_key='batterscouting.id', ondelete='CASCADE')
|
|
pitcherscouting_id: int | None = Field(default=None, foreign_key='pitcherscouting.id', ondelete='CASCADE')
|
|
variant: int | None = Field(default=0)
|
|
created: datetime.datetime = Field(default_factory=datetime.datetime.now, nullable=True)
|
|
|
|
|
|
class Card(CardBase, table=True):
|
|
player: Player = Relationship(back_populates='cards')
|
|
team: Team = Relationship(back_populates='cards')
|
|
lineups: list['Lineup'] = Relationship(back_populates='card', cascade_delete=True)
|
|
batterscouting: BatterScouting = Relationship(back_populates='cards')
|
|
pitcherscouting: PitcherScouting = Relationship(back_populates='cards')
|
|
|
|
|
|
class PositionRatingBase(SQLModel):
|
|
__table_args__ = (UniqueConstraint("player_id", "variant", "position"),)
|
|
id: int | None = Field(default=None, primary_key=True)
|
|
player_id: int = Field(foreign_key='player.id', index=True, ondelete='CASCADE')
|
|
variant: int = Field(default=0, index=True)
|
|
position: str = Field(index=True, include=['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF'])
|
|
innings: int = Field(default=0)
|
|
range: int = Field(default=5)
|
|
error: int = Field(default=0)
|
|
arm: int | None = Field(default=None)
|
|
pb: int | None = Field(default=None)
|
|
overthrow: int | None = Field(default=None)
|
|
created: datetime.datetime = Field(default_factory=datetime.datetime.now, nullable=True)
|
|
|
|
|
|
class PositionRating(PositionRatingBase, table=True):
|
|
player: Player = Relationship(back_populates='positions')
|
|
|
|
|
|
class Lineup(SQLModel, table=True):
|
|
id: int | None = Field(default=None, primary_key=True)
|
|
position: str = Field(index=True)
|
|
batting_order: int = Field(index=True)
|
|
after_play: int | None = Field(default=0)
|
|
replacing_id: int | None = Field(default=None)
|
|
active: bool = Field(default=True, index=True)
|
|
is_fatigued: bool | None = Field(default=None)
|
|
|
|
game_id: int = Field(foreign_key='game.id', index=True, ondelete='CASCADE')
|
|
game: Game = Relationship(back_populates='lineups')
|
|
|
|
team_id: int = Field(foreign_key='team.id', index=True, ondelete='CASCADE')
|
|
team: Team = Relationship(back_populates='lineups')
|
|
|
|
player_id: int = Field(foreign_key='player.id', index=True, ondelete='CASCADE')
|
|
player: Player = Relationship(back_populates='lineups')
|
|
|
|
card_id: int = Field(foreign_key='card.id', index=True, ondelete='CASCADE')
|
|
card: Card = Relationship(back_populates='lineups')
|
|
|
|
@field_validator('position')
|
|
def uppercase_strings(cls, value: str) -> str:
|
|
return value.upper()
|
|
|
|
# TODO: add function to return string value of game stats
|
|
|
|
|
|
class PlayBase(SQLModel):
|
|
id: int | None = Field(default=None, primary_key=True)
|
|
game_id: int = Field(foreign_key='game.id')
|
|
play_num: int
|
|
batter_id: int = Field(foreign_key='lineup.id')
|
|
pitcher_id: int = Field(foreign_key='lineup.id')
|
|
on_base_code: int = Field(default=0)
|
|
inning_half: str = Field(default='top')
|
|
inning_num: int = Field(default=1, ge=1)
|
|
batting_order: int = Field(default=1, ge=1, le=9)
|
|
starting_outs: int = Field(default=0, ge=0, le=2)
|
|
away_score: int = Field(default=0, ge=0)
|
|
home_score: int = Field(default=0, ge=0)
|
|
batter_pos: str | None = Field(default=None)
|
|
in_pow: bool = Field(default=False)
|
|
|
|
on_first_id: int | None = Field(default=None, foreign_key='lineup.id')
|
|
on_first_final: int | None = Field(default=None) # None = out, 1-4 = base
|
|
on_second_id: int | None = Field(default=None, foreign_key='lineup.id')
|
|
on_second_final: int | None = Field(default=None) # None = out, 1-4 = base
|
|
on_third_id: int | None = Field(default=None, foreign_key='lineup.id')
|
|
on_third_final: int | None = Field(default=None) # None = out, 1-4 = base
|
|
batter_final: int | None = Field(default=None) # None = out, 1-4 = base
|
|
|
|
pa: int = Field(default=1, ge=0, le=1)
|
|
ab: int = Field(default=1, ge=0, le=1)
|
|
run: int = Field(default=0, ge=0, le=1)
|
|
e_run: int = Field(default=0, ge=0, le=1)
|
|
hit: int = Field(default=0, ge=0, le=1)
|
|
rbi: int = Field(default=0)
|
|
double: int = Field(default=0, ge=0, le=1)
|
|
triple: int = Field(default=0, ge=0, le=1)
|
|
homerun: int = Field(default=0, ge=0, le=1)
|
|
bb: int = Field(default=0, ge=0, le=1)
|
|
so: int = Field(default=0, ge=0, le=1)
|
|
hbp: int = Field(default=0, ge=0, le=1)
|
|
sac: int = Field(default=0, ge=0, le=1)
|
|
ibb: int = Field(default=0, ge=0, le=1)
|
|
gidp: int = Field(default=0, ge=0, le=1)
|
|
bphr: int = Field(default=0, ge=0, le=1)
|
|
bpfo: int = Field(default=0, ge=0, le=1)
|
|
bp1b: int = Field(default=0, ge=0, le=1)
|
|
bplo: int = Field(default=0, ge=0, le=1)
|
|
sb: int = Field(default=0, ge=0, le=1)
|
|
cs: int = Field(default=0, ge=0, le=1)
|
|
outs: int = Field(default=0, ge=0, le=3)
|
|
|
|
wpa: float = Field(default=0)
|
|
re24: float = Field(default=0)
|
|
|
|
catcher_id: int = Field(foreign_key='lineup.id')
|
|
defender_id: int | None = Field(default=None, foreign_key='lineup.id')
|
|
runner_id: int | None = Field(default=None, foreign_key='lineup.id')
|
|
|
|
check_pos: str | None = Field(default=None)
|
|
error: int = Field(default=0)
|
|
wild_pitch: int = Field(default=0, ge=0, le=1)
|
|
passed_ball: int = Field(default=0, ge=0, le=1)
|
|
pick_off: int = Field(default=0, ge=0, le=1)
|
|
balk: int = Field(default=0, ge=0, le=1)
|
|
complete: bool = Field(default=False)
|
|
locked: bool = Field(default=False)
|
|
is_go_ahead: bool = Field(default=False)
|
|
is_tied: bool = Field(default=False)
|
|
is_new_inning: bool = Field(default=False)
|
|
managerai_id: int | None = Field(default=None, foreign_key='managerai.id')
|
|
|
|
@field_validator('inning_half')
|
|
def lowercase_strings(cls, value: str) -> str:
|
|
return value.lower()
|
|
|
|
@field_validator('check_pos', 'batter_pos')
|
|
def uppercase_strings(cls, value: str) -> str:
|
|
return value.upper()
|
|
|
|
def ai_run_diff(self):
|
|
if self.game.ai_team == 'away':
|
|
return self.away_score - self.home_score
|
|
else:
|
|
return self.home_score - self.away_score
|
|
|
|
|
|
class Play(PlayBase, table=True):
|
|
game: Game = Relationship(back_populates='plays')
|
|
batter: Lineup = Relationship(
|
|
sa_relationship_kwargs=dict(foreign_keys="[Play.batter_id]")
|
|
)
|
|
pitcher: Lineup = Relationship(
|
|
sa_relationship_kwargs=dict(foreign_keys="[Play.pitcher_id]")
|
|
)
|
|
on_first: Lineup = Relationship(
|
|
sa_relationship_kwargs=dict(foreign_keys="[Play.on_first_id]")
|
|
)
|
|
on_second: Lineup = Relationship(
|
|
sa_relationship_kwargs=dict(foreign_keys="[Play.on_second_id]")
|
|
)
|
|
on_third: Lineup = Relationship(
|
|
sa_relationship_kwargs=dict(foreign_keys="[Play.on_third_id]")
|
|
)
|
|
catcher: Lineup = Relationship(
|
|
sa_relationship_kwargs=dict(foreign_keys="[Play.catcher_id]")
|
|
)
|
|
defender: Lineup = Relationship(
|
|
sa_relationship_kwargs=dict(foreign_keys="[Play.defender_id]")
|
|
)
|
|
runner: Lineup = Relationship(
|
|
sa_relationship_kwargs=dict(foreign_keys="[Play.runner_id]")
|
|
)
|
|
managerai: ManagerAi = Relationship(back_populates='plays')
|
|
|
|
def init_ai(self, session: Session):
|
|
id = ((datetime.datetime.now().day * self.batter.team.id) % 3) + 1
|
|
if id > 3 or id < 1:
|
|
self.managerai_id = 1
|
|
else:
|
|
self.managerai_id = id
|
|
|
|
session.add(self)
|
|
session.commit()
|
|
|
|
@property
|
|
def scorebug_ascii(self):
|
|
occupied = '●'
|
|
unoccupied = '○'
|
|
|
|
first_base = unoccupied if not self.on_first else occupied
|
|
second_base = unoccupied if not self.on_second else occupied
|
|
third_base = unoccupied if not self.on_third else occupied
|
|
half = '▲' if self.inning_half == 'top' else '▼'
|
|
|
|
if self.game.active:
|
|
inning = f'{half} {self.inning_num}'
|
|
outs = f'{self.starting_outs} Out{"s" if self.starting_outs != 1 else ""}'
|
|
else:
|
|
inning = f'F/{self.inning_num if self.inning_half == "bot" else self.inning_num - 1}'
|
|
outs = ''
|
|
|
|
game_string = f'```\n' \
|
|
f'{self.game.away_team.abbrev.replace("Gauntlet-", ""): ^5}{self.away_score: ^3} {second_base}{inning: >10}\n' \
|
|
f'{self.game.home_team.abbrev.replace("Gauntlet-", ""): ^5}{self.home_score: ^3} {third_base} {first_base}{outs: >8}\n```'
|
|
|
|
return game_string
|
|
|
|
@property
|
|
def pitching_ai_note(self) -> str:
|
|
ai_note = ''
|
|
# Holding Baserunners
|
|
if self.starting_outs == 2 and self.on_base_code > 0:
|
|
if self.on_base_code == 1:
|
|
ai_note += f'- hold {self.on_first.player.name}\n'
|
|
elif self.on_base_code == 2:
|
|
ai_note += f'- hold {self.on_second.player.name}\n'
|
|
elif self.on_base_code in [4, 5, 7]:
|
|
ai_note += f'- hold {self.on_first.player.name} on first\n'
|
|
# elif self.on_base_code == 5:
|
|
# ai_note += f'- hold the runner on first\n'
|
|
elif self.on_base_code == 6:
|
|
ai_note += f'- hold {self.on_second.player.name} on 2nd\n'
|
|
elif self.on_base_code in [1, 5]:
|
|
runner = self.on_first.player
|
|
if self.on_first.card.batterscouting.battingcard.steal_auto:
|
|
ai_note += f'- hold {runner.name} on 1st\n'
|
|
elif self.on_base_code in [2, 4]:
|
|
if self.on_second.card.batterscouting.battingcard.steal_low + max(self.pitcher.card.pitcherscouting.pitchingcard.hold, 5) >= 14:
|
|
ai_note += f'- hold {self.on_second.player.name} on 2nd\n'
|
|
|
|
# Defensive Alignment
|
|
if self.on_third and self.starting_outs < 2:
|
|
if self.could_walkoff:
|
|
ai_note += f'- play the outfield and infield in'
|
|
elif abs(self.away_score - self.home_score) <= 3:
|
|
ai_note += f'- play the whole infield in\n'
|
|
else:
|
|
ai_note += f'- play the corners in\n'
|
|
|
|
if len(ai_note) == 0 and self.on_base_code > 0:
|
|
ai_note += f'- play straight up\n'
|
|
|
|
return ai_note
|
|
|
|
@property
|
|
def batting_ai_note(self) -> str:
|
|
ai_note = '' # TODO: migrate Manager AI to their own local model
|
|
|
|
return ai_note
|
|
|
|
@property
|
|
def ai_note(self) -> str: # TODO: test these three functions with specific OBCs
|
|
if self.inning_half == 'top':
|
|
if self.game.ai_team == 'away':
|
|
return self.batting_ai_note
|
|
else:
|
|
return self.pitching_ai_note
|
|
else:
|
|
if self.game.ai_team == 'away':
|
|
return self.pitching_ai_note
|
|
else:
|
|
return self.batting_ai_note
|
|
|
|
@property
|
|
def ai_is_batting(self) -> bool:
|
|
if self.game.ai_team is None:
|
|
return False
|
|
|
|
if (self.game.ai_team == 'away' and self.inning_half == 'top') or (self.game.ai_team == 'home' and self.inning_half == 'bot'):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
@property
|
|
def could_walkoff(self) -> bool:
|
|
return False
|
|
|
|
|
|
"""
|
|
BEGIN DEVELOPMENT HELPERS
|
|
"""
|
|
|
|
|
|
def create_db_and_tables():
|
|
SQLModel.metadata.create_all(engine)
|
|
ManagerAi.create_ai()
|
|
|
|
|
|
def create_test_games():
|
|
with Session(engine) as session:
|
|
game_1 = Game(
|
|
away_team_id=1,
|
|
home_team_id=2,
|
|
channel_id=1234,
|
|
season=9,
|
|
)
|
|
game_2 = Game(
|
|
away_team_id=3,
|
|
home_team_id=4,
|
|
channel_id=5678,
|
|
season=9,
|
|
)
|
|
|
|
cardset_2024 = Cardset(name='2024 Season', ranked_legal=True)
|
|
cardset_2022 = Cardset(name='2022 Season', ranked_legal=False)
|
|
|
|
game_1_cardset_2024_link = GameCardsetLink(game=game_1, cardset=cardset_2024, priority=1)
|
|
game_1_cardset_2022_link = GameCardsetLink(game=game_1, cardset=cardset_2022, priority=2)
|
|
game_2_cardset_2024_link = GameCardsetLink(game=game_2, cardset=cardset_2024, priority=1)
|
|
|
|
for team_id in [1, 2]:
|
|
for (order, pos) in [(1, 'C'), (2, '1B'), (3, '2B'), (4, '3B'), (5, 'SS'), (6, 'LF'), (7, 'CF'), (8, 'RF'), (9, 'DH')]:
|
|
this_lineup = Lineup(team_id=team_id, card_id=order, player_id=68+order, position=pos, batting_order=order, game=game_1)
|
|
|
|
for team_id in [3, 4]:
|
|
for (order, pos) in [(1, 'C'), (2, '1B'), (3, '2B'), (4, '3B'), (5, 'SS'), (6, 'LF'), (7, 'CF'), (8, 'RF'), (9, 'DH')]:
|
|
this_lineup = Lineup(team_id=team_id, card_id=order, player_id=100+order, position=pos, batting_order=order, game=game_2)
|
|
|
|
session.add(game_1)
|
|
session.add(game_2)
|
|
session.commit()
|
|
|
|
|
|
def select_speed_testing():
|
|
with Session(engine) as session:
|
|
game_1 = session.exec(select(Game).where(Game.id == 1)).one()
|
|
ss_search_start = datetime.datetime.now()
|
|
man_ss = [x for x in game_1.lineups if x.position == 'SS' and x.active]
|
|
ss_search_end = datetime.datetime.now()
|
|
|
|
ss_query_start = datetime.datetime.now()
|
|
query_ss = session.exec(select(Lineup).where(Lineup.game == game_1, Lineup.position == 'SS', Lineup.active == True)).all()
|
|
ss_query_end = datetime.datetime.now()
|
|
|
|
manual_time = ss_search_end - ss_search_start
|
|
query_time = ss_query_end - ss_query_start
|
|
|
|
print(f'Manual Shortstops: time: {manual_time.microseconds} ms / {man_ss}')
|
|
print(f'Query Shortstops: time: {query_time.microseconds} ms / {query_ss}')
|
|
print(f'Game: {game_1}')
|
|
|
|
games = session.exec(select(Game).where(Game.active == True)).all()
|
|
print(f'len(games): {len(games)}')
|
|
|
|
|
|
def select_all_testing():
|
|
with Session(engine) as session:
|
|
game_search = session.exec(select(Team)).all()
|
|
for game in game_search:
|
|
print(f'Game: {game}')
|
|
|
|
|
|
# def select_specic_fields():
|
|
# with Session(engine) as session:
|
|
# games = session.exec(select(Game.id, Game.away_team, Game.home_team))
|
|
# print(f'Games: {games}')
|
|
# print(f'.all(): {games.all()}')
|
|
|
|
|
|
def main():
|
|
create_db_and_tables()
|
|
create_test_games()
|
|
# select_speed_testing()
|
|
# select_all_testing()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|