Update api logging

New Position exception
Pull scouting data with lineups
More bunt types
String validation on gameplay models
AI Defensive alignment
This commit is contained in:
Cal Corum 2024-11-16 00:31:54 -06:00
parent bfd72ae0f5
commit 3d333dabc3
12 changed files with 956 additions and 93 deletions

View File

@ -39,14 +39,23 @@ def get_req_url(endpoint: str, api_ver: int = 2, object_id: int = None, params:
def log_return_value(log_string: str): def log_return_value(log_string: str):
if master_debug: start = 0
logger.info(f'return: {log_string[:1200]}{" [ S N I P P E D ]" if len(log_string) > 1200 else ""}\n') end = 3000
else: while end < 300000:
logger.debug(f'return: {log_string[:1200]}{" [ S N I P P E D ]" if len(log_string) > 1200 else ""}\n') line = log_string[start:end]
if len(line) == 0:
return
logger.info(f'{"\n\nreturn: " if start == 0 else ""}{log_string[start:end]}')
start += 3000
end += 3000
logger.warning('[ S N I P P E D ]')
# if master_debug:
# logger.info(f'return: {log_string[:1200]}{" [ S N I P P E D ]" if len(log_string) > 1200 else ""}\n')
# else:
# logger.debug(f'return: {log_string[:1200]}{" [ S N I P P E D ]" if len(log_string) > 1200 else ""}\n')
async def db_get(endpoint: str, api_ver: int = 2, object_id: int = None, params: list = None, none_okay: bool = True, async def db_get(endpoint: str, api_ver: int = 2, object_id: int = None, params: list = None, none_okay: bool = True, timeout: int = 3):
timeout: int = 3):
req_url = get_req_url(endpoint, api_ver=api_ver, object_id=object_id, params=params) req_url = get_req_url(endpoint, api_ver=api_ver, object_id=object_id, params=params)
log_string = f'db_get - get: {endpoint} id: {object_id} params: {params}' log_string = f'db_get - get: {endpoint} id: {object_id} params: {params}'
logger.info(log_string) if master_debug else logger.debug(log_string) logger.info(log_string) if master_debug else logger.debug(log_string)

View File

@ -16,7 +16,7 @@ from helpers import DEFENSE_LITERAL, PD_PLAYERS_ROLE_NAME, get_channel, team_rol
from in_game.ai_manager import get_starting_pitcher, get_starting_lineup from in_game.ai_manager import get_starting_pitcher, get_starting_lineup
from in_game.game_helpers import PUBLIC_FIELDS_CATEGORY_NAME, legal_check from in_game.game_helpers import PUBLIC_FIELDS_CATEGORY_NAME, legal_check
from in_game.gameplay_models import Lineup, Play, Session, engine, player_description, select, Game from in_game.gameplay_models import Lineup, Play, Session, engine, player_description, select, Game
from in_game.gameplay_queries import get_channel_game_or_none, get_active_games_by_team, get_game_lineups, get_team_or_none, get_card_or_none from in_game.gameplay_queries import get_and_cache_position, get_channel_game_or_none, get_active_games_by_team, get_game_lineups, get_team_or_none, get_card_or_none
from utilities.buttons import Confirm, ask_confirm from utilities.buttons import Confirm, ask_confirm
@ -52,7 +52,7 @@ class Gameplay(commands.Cog):
async def post_play(self, session: Session, interaction: discord.Interaction, this_play: Play, buffer_message: str = None): async def post_play(self, session: Session, interaction: discord.Interaction, this_play: Play, buffer_message: str = None):
if is_game_over(this_play): if is_game_over(this_play):
await interaction.edit_original_response(content=f'Looks like this one is over! ') await interaction.edit_original_response(content=f'Looks like this one is over!')
submit_game = await ask_confirm( submit_game = await ask_confirm(
interaction=interaction, interaction=interaction,
question=f'Final score: {this_play.game.away_team.abbrev} {this_play.away_score} - {this_play.home_score} {this_play.game.home_team.abbrev}\n{this_play.scorebug_ascii}\nShould I go ahead and submit this game or roll it back a play?', question=f'Final score: {this_play.game.away_team.abbrev} {this_play.away_score} - {this_play.home_score} {this_play.game.home_team.abbrev}\n{this_play.scorebug_ascii}\nShould I go ahead and submit this game or roll it back a play?',
@ -211,6 +211,8 @@ class Gameplay(commands.Cog):
) )
return return
await get_and_cache_position(session, human_sp_card, 'P')
legal_data = await legal_check([sp_card_id], difficulty_name=league.value) legal_data = await legal_check([sp_card_id], difficulty_name=league.value)
if not legal_data['legal']: if not legal_data['legal']:
await interaction.edit_original_response( await interaction.edit_original_response(
@ -259,6 +261,12 @@ class Gameplay(commands.Cog):
# Commit game and lineups # Commit game and lineups
session.add(this_game) session.add(this_game)
session.commit() session.commit()
await final_message.edit(content=f'The {ai_team.sname} lineup is in, pulling in scouting data...')
for batter in batter_lineups:
if batter.position != 'DH':
await get_and_cache_position(session, batter.card, batter.position)
# session.refresh(this_game) # session.refresh(this_game)
away_role = await team_role(interaction, away_team) away_role = await team_role(interaction, away_team)
@ -366,11 +374,17 @@ class Gameplay(commands.Cog):
human_lineups = await get_lineups_from_sheets(session, self.sheets, this_game, this_team=lineup_team, lineup_num=lineup.name, roster_num=int(roster.value)) human_lineups = await get_lineups_from_sheets(session, self.sheets, this_game, this_team=lineup_team, lineup_num=lineup.name, roster_num=int(roster.value))
await interaction.edit_original_response(content='Heard from sheets, pulling in scouting data...')
for batter in human_lineups: for batter in human_lineups:
session.add(batter) session.add(batter)
session.commit() session.commit()
for batter in human_lineups:
if batter.position != 'DH':
await get_and_cache_position(session, batter.card, batter.position)
await interaction.edit_original_response(content=None, embed=this_game.get_scorebug_embed(session)) await interaction.edit_original_response(content=None, embed=this_game.get_scorebug_embed(session))
@app_commands.command(name='gamestate', description='Post the current game state') @app_commands.command(name='gamestate', description='Post the current game state')
@ -397,8 +411,8 @@ class Gameplay(commands.Cog):
with Session(engine) as session: with Session(engine) as session:
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log flyball') this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log flyball')
this_play = await flyballs(session, interaction, this_play, flyball_type)
logger.info(f'log flyball {flyball_type} - this_play: {this_play}') logger.info(f'log flyball {flyball_type} - this_play: {this_play}')
this_play = await flyballs(session, interaction, this_play, flyball_type)
await self.complete_and_post_play( await self.complete_and_post_play(
session, session,
@ -413,8 +427,8 @@ class Gameplay(commands.Cog):
with Session(engine) as session: with Session(engine) as session:
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log single') this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log single')
this_play = await singles(session, interaction, this_play, single_type)
logger.info(f'log single {single_type} - this_play: {this_play}') logger.info(f'log single {single_type} - this_play: {this_play}')
this_play = await singles(session, interaction, this_play, single_type)
await self.complete_and_post_play(session, interaction, this_play, buffer_message='Double logged' if ((this_play.on_first or this_play.on_second) and single_type == 'uncapped') else None) await self.complete_and_post_play(session, interaction, this_play, buffer_message='Double logged' if ((this_play.on_first or this_play.on_second) and single_type == 'uncapped') else None)
@ -437,8 +451,8 @@ class Gameplay(commands.Cog):
with Session(engine) as session: with Session(engine) as session:
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log double') this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log double')
this_play = await doubles(session, interaction, this_play, double_type)
logger.info(f'log double {double_type} - this_play: {this_play}') logger.info(f'log double {double_type} - this_play: {this_play}')
this_play = await doubles(session, interaction, this_play, double_type)
await self.complete_and_post_play(session, interaction, this_play, buffer_message='Double logged' if (this_play.on_first and double_type == 'uncapped') else None) await self.complete_and_post_play(session, interaction, this_play, buffer_message='Double logged' if (this_play.on_first and double_type == 'uncapped') else None)
@ -447,8 +461,8 @@ class Gameplay(commands.Cog):
with Session(engine) as session: with Session(engine) as session:
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log triple') this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log triple')
this_play = await triples(session, interaction, this_play)
logger.info(f'log triple - this_play: {this_play}') logger.info(f'log triple - this_play: {this_play}')
this_play = await triples(session, interaction, this_play)
await self.complete_and_post_play(session, interaction, this_play) await self.complete_and_post_play(session, interaction, this_play)
@ -457,8 +471,8 @@ class Gameplay(commands.Cog):
with Session(engine) as session: with Session(engine) as session:
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log homerun') this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log homerun')
this_play = await homeruns(session, interaction, this_play, homerun_type)
logger.info(f'log homerun {homerun_type} - this_play: {this_play}') logger.info(f'log homerun {homerun_type} - this_play: {this_play}')
this_play = await homeruns(session, interaction, this_play, homerun_type)
await self.complete_and_post_play(session, interaction, this_play) await self.complete_and_post_play(session, interaction, this_play)
@ -467,8 +481,8 @@ class Gameplay(commands.Cog):
with Session(engine) as session: with Session(engine) as session:
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log walk') this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log walk')
this_play = await walks(session, interaction, this_play, walk_type)
logger.info(f'log walk {walk_type} - this_play: {this_play}') logger.info(f'log walk {walk_type} - this_play: {this_play}')
this_play = await walks(session, interaction, this_play, walk_type)
await self.complete_and_post_play(session, interaction, this_play) await self.complete_and_post_play(session, interaction, this_play)
@ -477,8 +491,8 @@ class Gameplay(commands.Cog):
with Session(engine) as session: with Session(engine) as session:
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log strikeout') this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log strikeout')
this_play = await strikeouts(session, interaction, this_play)
logger.info(f'log strikeout - this_play: {this_play}') logger.info(f'log strikeout - this_play: {this_play}')
this_play = await strikeouts(session, interaction, this_play)
await self.complete_and_post_play(session, interaction, this_play) await self.complete_and_post_play(session, interaction, this_play)
@ -487,8 +501,8 @@ class Gameplay(commands.Cog):
with Session(engine) as session: with Session(engine) as session:
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log popout') this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log popout')
this_play = await popouts(session, interaction, this_play)
logger.info(f'log popout - this_play: {this_play}') logger.info(f'log popout - this_play: {this_play}')
this_play = await popouts(session, interaction, this_play)
await self.complete_and_post_play(session, interaction, this_play) await self.complete_and_post_play(session, interaction, this_play)
@ -497,18 +511,29 @@ class Gameplay(commands.Cog):
with Session(engine) as session: with Session(engine) as session:
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log hit-by-pitch') this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log hit-by-pitch')
this_play = await hit_by_pitch(session, interaction, this_play)
logger.info(f'log hit-by-pitch - this_play: {this_play}') logger.info(f'log hit-by-pitch - this_play: {this_play}')
this_play = await hit_by_pitch(session, interaction, this_play)
await self.complete_and_post_play(session, interaction, this_play) await self.complete_and_post_play(session, interaction, this_play)
@group_log.command(name='bunt', description='Hit by pitch: batter to first; runners advance if forced') @group_log.command(name='bunt', description='Bunts: sacrifice, bad, popout, double-play, defense')
async def log_sac_bunt(self, interaction: discord.Interaction, bunt_type: Literal['sacrifice', 'bad', 'popout', 'double-play', 'defense']): async def log_sac_bunt(self, interaction: discord.Interaction, bunt_type: Literal['sacrifice', 'bad', 'popout', 'double-play', 'defense']):
with Session(engine) as session: with Session(engine) as session:
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log bunt') this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log bunt')
this_play = await bunts(session, interaction, this_play, bunt_type) if this_play.on_base_code == 0:
await interaction.edit_original_response(
content=f'You cannot bunt when the bases are empty.'
)
return
elif this_play.starting_outs == 2:
await interaction.edit_original_response(
content=f'You cannot bunt with two outs.'
)
return
logger.info(f'log bunt - this_play: {this_play}') logger.info(f'log bunt - this_play: {this_play}')
this_play = await bunts(session, interaction, this_play, bunt_type)
await self.complete_and_post_play(session, interaction, this_play) await self.complete_and_post_play(session, interaction, this_play)
@ -517,8 +542,8 @@ class Gameplay(commands.Cog):
with Session(engine) as session: with Session(engine) as session:
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log undo-play') this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='log undo-play')
this_play = undo_play(session, this_play)
logger.info(f'log undo-play - this_play: {this_play}') logger.info(f'log undo-play - this_play: {this_play}')
this_play = undo_play(session, this_play)
await self.post_play(session, interaction, this_play) await self.post_play(session, interaction, this_play)
@ -528,8 +553,8 @@ class Gameplay(commands.Cog):
with Session(engine) as session: with Session(engine) as session:
this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='show-card defense') this_game, owner_team, this_play = await checks_log_interaction(session, interaction, command_name='show-card defense')
await show_defense_cards(session, interaction, this_play, position)
logger.info(f'show-card defense - position: {position}') logger.info(f'show-card defense - position: {position}')
await show_defense_cards(session, interaction, this_play, position)
async def setup(bot): async def setup(bot):

View File

@ -21,6 +21,16 @@ from utilities.pages import Pagination
logger = logging.getLogger('discord_app') logger = logging.getLogger('discord_app')
WPA_DF = pd.read_csv(f'storage/wpa_data.csv').set_index('index') WPA_DF = pd.read_csv(f'storage/wpa_data.csv').set_index('index')
TO_BASE = {
2: 'to second',
3: 'to third',
4: 'home'
}
AT_BASE = {
2: 'at second',
3: 'at third',
4: 'at home'
}
def get_obc(on_first = None, on_second = None, on_third = None) -> int: def get_obc(on_first = None, on_second = None, on_third = None) -> int:
@ -112,7 +122,7 @@ def complete_play(session:Session, this_play: Play):
switch_sides = True switch_sides = True
obc = 0 obc = 0
nso = 0 nso = 0
nih = 'bot' if this_play.inning_half.lower() == 'top' else 'top' nih = 'bot' if this_play.inning_half == 'top' else 'top'
away_score = this_play.away_score away_score = this_play.away_score
home_score = this_play.home_score home_score = this_play.home_score
@ -652,16 +662,6 @@ async def check_uncapped_advance(session: Session, interaction: discord.Interact
outfielder = await show_outfield_cards(session, interaction, this_play) outfielder = await show_outfield_cards(session, interaction, this_play)
logger.info(f'throw from {outfielder.player.name_with_desc}') logger.info(f'throw from {outfielder.player.name_with_desc}')
def_team = this_play.pitcher.team def_team = this_play.pitcher.team
TO_BASE = {
2: 'to second',
3: 'to third',
4: 'home'
}
AT_BASE = {
2: 'at second',
3: 'at third',
4: 'at home'
}
# Either there is no AI team or the AI is pitching # Either there is no AI team or the AI is pitching
if not this_game.ai_team or not this_play.ai_is_batting: if not this_game.ai_team or not this_play.ai_is_batting:
@ -1072,11 +1072,37 @@ async def hit_by_pitch(session: Session, interaction: discord.Interaction, this_
async def bunts(session: Session, interaction: discord.Interaction, this_play: Play, bunt_type: Literal['sacrifice', 'bad', 'popout', 'double-play', 'defense']): 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.ab = 1 if bunt_type != 'sacrifice' else 0
this_play.sac = 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': if bunt_type == 'sacrifice':
this_play = advance_runners(session, this_play, num_bases=1) this_play = advance_runners(session, this_play, num_bases=1)
elif bunt_type == 'popout': elif bunt_type == 'popout':
this_play = advance_runners(session, this_play, num_bases=0) 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
else: else:
log_exception(KeyError, f'Bunt type {bunt_type} is not yet implemented') log_exception(KeyError, f'Bunt type {bunt_type} is not yet implemented')
@ -1255,6 +1281,36 @@ async def get_game_summary_embed(session: Session, interaction: discord.Interact
inline=False 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:
poop_line = f'{player_name} - '
player_name = f'{get_player_name_from_dict(tp['player'])}'
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', {tp["re24"]:.2f} re24\n'
poop_string += poop_line
if len(poop_string) > 0:
game_embed.add_field(
'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' pit_string = f'Win: {game_summary["pitchers"]["win"]["p_name"]}\nLoss: {game_summary["pitchers"]["loss"]["p_name"]}\n'
hold_string = None hold_string = None

View File

@ -49,3 +49,6 @@ class PlayInitException(GameException):
class DatabaseError(GameException): class DatabaseError(GameException):
pass pass
class PositionNotFoundException(GameException):
pass

View File

@ -12,8 +12,7 @@ from peewee import *
from typing import Optional, Literal from typing import Optional, Literal
from in_game import data_cache from in_game import data_cache
import in_game.gameplay_models as iggm from in_game.gameplay_models import Play, Session, Game, Team, Lineup
from in_game.gameplay_models import Play, Session, Game, Team
from in_game.gameplay_queries import get_or_create_ai_card, get_player_id_from_dict, get_player_or_none from in_game.gameplay_queries import get_or_create_ai_card, get_player_id_from_dict, get_player_or_none
db = SqliteDatabase( db = SqliteDatabase(
@ -42,28 +41,28 @@ class BaseModel(Model):
database = db database = db
class Lineup(BaseModel): # class Lineup(BaseModel):
team_id = IntegerField() # team_id = IntegerField()
game_level = CharField() # 'minor', 'major', 'hof' # game_level = CharField() # 'minor', 'major', 'hof'
vs_hand = CharField() # 'left', 'right' # vs_hand = CharField() # 'left', 'right'
bat_one_card = IntegerField() # bat_one_card = IntegerField()
bat_one_pos = CharField() # bat_one_pos = CharField()
bat_two_card = IntegerField() # bat_two_card = IntegerField()
bat_two_pos = CharField() # bat_two_pos = CharField()
bat_three_card = IntegerField() # bat_three_card = IntegerField()
bat_three_pos = CharField() # bat_three_pos = CharField()
bat_four_card = IntegerField() # bat_four_card = IntegerField()
bat_four_pos = CharField() # bat_four_pos = CharField()
bat_five_card = IntegerField() # bat_five_card = IntegerField()
bat_five_pos = CharField() # bat_five_pos = CharField()
bat_six_card = IntegerField() # bat_six_card = IntegerField()
bat_six_pos = CharField() # bat_six_pos = CharField()
bat_seven_card = IntegerField() # bat_seven_card = IntegerField()
bat_seven_pos = CharField() # bat_seven_pos = CharField()
bat_eight_card = IntegerField() # bat_eight_card = IntegerField()
bat_eight_pos = CharField() # bat_eight_pos = CharField()
bat_nine_card = IntegerField() # bat_nine_card = IntegerField()
bat_nine_pos = CharField() # bat_nine_pos = CharField()
class Rotation(BaseModel): class Rotation(BaseModel):
@ -300,7 +299,7 @@ async def get_starting_lineup(session: Session, team: Team, game: Game, league_n
this_player = await get_player_or_none(session, get_player_id_from_dict(y[1]['player'])) this_player = await get_player_or_none(session, get_player_id_from_dict(y[1]['player']))
this_card = await get_or_create_ai_card(session, player=this_player, team=team) this_card = await get_or_create_ai_card(session, player=this_player, team=team)
lineups.append(iggm.Lineup( lineups.append(Lineup(
position=y[0], position=y[0],
batting_order=i, batting_order=i,
game=game, game=game,
@ -316,7 +315,7 @@ async def get_starting_lineup(session: Session, team: Team, game: Game, league_n
async def get_starting_pitcher( async def get_starting_pitcher(
session: Session, this_team: Team, this_game: Game, is_home: bool, league_name: str) -> iggm.Lineup: session: Session, this_team: Team, this_game: Game, is_home: bool, league_name: str) -> Lineup:
d_100 = random.randint(1, 100) d_100 = random.randint(1, 100)
if is_home: if is_home:
if d_100 <= 30: if d_100 <= 30:
@ -347,7 +346,7 @@ async def get_starting_pitcher(
this_player = await get_player_or_none(session, get_player_id_from_dict(sp_query)) this_player = await get_player_or_none(session, get_player_id_from_dict(sp_query))
sp_card = await get_or_create_ai_card(session, this_player, this_team) sp_card = await get_or_create_ai_card(session, this_player, this_team)
return iggm.Lineup( return Lineup(
team=this_team, team=this_team,
player=this_player, player=this_player,
card=sp_card, card=sp_card,

View File

@ -5,12 +5,12 @@ from typing import Literal
import discord import discord
import pydantic import pydantic
from sqlmodel import Session, SQLModel, create_engine, select, or_, Field, Relationship from pydantic import field_validator
from sqlmodel import Session, SQLModel, UniqueConstraint, create_engine, select, or_, Field, Relationship, text
from sqlalchemy import func, desc from sqlalchemy import func, desc
from api_calls import db_get, db_post
from exceptions import * from exceptions import *
from in_game.managerai_responses import JumpResponse, TagResponse, ThrowResponse, UncappedRunResponse from in_game.managerai_responses import DefenseResponse, JumpResponse, TagResponse, ThrowResponse, UncappedRunResponse
logger = logging.getLogger('discord_app') logger = logging.getLogger('discord_app')
@ -65,7 +65,7 @@ class TeamBase(SQLModel):
ranking: int ranking: int
has_guide: bool has_guide: bool
is_ai: bool is_ai: bool
created: datetime.datetime | None = Field(default=datetime.datetime.now()) created: datetime.datetime = Field(default_factory=datetime.datetime.now, nullable=True)
@property @property
def description(self) -> str: def description(self) -> str:
@ -128,6 +128,10 @@ class Game(SQLModel, table=True):
) )
lineups: list['Lineup'] = Relationship(back_populates='game', cascade_delete=True) lineups: list['Lineup'] = Relationship(back_populates='game', cascade_delete=True)
plays: list['Play'] = 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 @property
def cardset_param_string(self) -> str: def cardset_param_string(self) -> str:
@ -363,7 +367,7 @@ class ManagerAi(ManagerAiBase, table=True):
this_resp = JumpResponse() this_resp = JumpResponse()
this_play = this_game.current_play_or_none(session) this_play = this_game.current_play_or_none(session)
if this_play is None: if this_play is None:
raise KeyError(f'No game found while checking for jump') raise GameException(f'No game found while checking for jump')
num_outs = this_play.starting_outs num_outs = this_play.starting_outs
run_diff = this_play.away_score - this_play.home_score run_diff = this_play.away_score - this_play.home_score
@ -418,7 +422,7 @@ class ManagerAi(ManagerAiBase, table=True):
this_resp = TagResponse() this_resp = TagResponse()
this_play = this_game.current_play_or_none(session) this_play = this_game.current_play_or_none(session)
if this_play is None: if this_play is None:
raise KeyError(f'No game found while checking tag_from_second') raise GameException(f'No game found while checking tag_from_second')
ai_rd = this_play.ai_run_diff() ai_rd = this_play.ai_run_diff()
aggression_mod = abs(self.ahead_aggression - 5 if ai_rd > 0 else self.behind_aggression - 5) aggression_mod = abs(self.ahead_aggression - 5 if ai_rd > 0 else self.behind_aggression - 5)
@ -442,7 +446,7 @@ class ManagerAi(ManagerAiBase, table=True):
this_resp = ThrowResponse() this_resp = ThrowResponse()
this_play = this_game.current_play_or_none(session) this_play = this_game.current_play_or_none(session)
if this_play is None: if this_play is None:
raise KeyError(f'No game found while checking throw_at_uncapped') raise GameException(f'No game found while checking throw_at_uncapped')
ai_rd = this_play.ai_run_diff() ai_rd = this_play.ai_run_diff()
aggression = self.ahead_aggression if ai_rd > 0 else self.behind_aggression aggression = self.ahead_aggression if ai_rd > 0 else self.behind_aggression
@ -481,7 +485,7 @@ class ManagerAi(ManagerAiBase, table=True):
this_resp = UncappedRunResponse() this_resp = UncappedRunResponse()
this_play = this_game.current_play_or_none(session) this_play = this_game.current_play_or_none(session)
if this_play is None: if this_play is None:
raise KeyError(f'No game found while checking uncapped_advance_lead') raise GameException(f'No game found while checking uncapped_advance_lead')
ai_rd = this_play.ai_run_diff() ai_rd = this_play.ai_run_diff()
aggression = self.ahead_aggression - 5 if ai_rd > 0 else self.behind_aggression - 5 aggression = self.ahead_aggression - 5 if ai_rd > 0 else self.behind_aggression - 5
@ -525,6 +529,47 @@ class ManagerAi(ManagerAiBase, table=True):
return this_resp 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): class CardsetBase(SQLModel):
id: int | None = Field(default=None, primary_key=True) id: int | None = Field(default=None, primary_key=True)
@ -564,7 +609,14 @@ class PlayerBase(SQLModel):
bbref_id: str | None = Field(default=None) bbref_id: str | None = Field(default=None)
fangr_id: str | None = Field(default=None) fangr_id: str | None = Field(default=None)
mlbplayer_id: int | None = Field(default=None) mlbplayer_id: int | None = Field(default=None)
created: datetime.datetime | None = Field(default=datetime.datetime.now()) 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 @property
def p_card_url(self): def p_card_url(self):
@ -597,6 +649,7 @@ class Player(PlayerBase, table=True):
cardset: Cardset = Relationship(back_populates='players') cardset: Cardset = Relationship(back_populates='players')
cards: list['Card'] = Relationship(back_populates='player', cascade_delete=True) cards: list['Card'] = Relationship(back_populates='player', cascade_delete=True)
lineups: list['Lineup'] = 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 @property
def name_with_desc(self): def name_with_desc(self):
@ -620,18 +673,202 @@ def player_description(player: Player = None, player_dict: dict = None) -> str:
return r_val 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): class CardBase(SQLModel):
id: int | None = Field(default=None, primary_key=True) id: int | None = Field(default=None, primary_key=True)
player_id: int = Field(foreign_key='player.id', index=True, ondelete='CASCADE') player_id: int = Field(foreign_key='player.id', index=True, ondelete='CASCADE')
team_id: int = Field(foreign_key='team.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) variant: int | None = Field(default=0)
created: datetime.datetime | None = Field(default=datetime.datetime.now()) created: datetime.datetime = Field(default_factory=datetime.datetime.now, nullable=True)
class Card(CardBase, table=True): class Card(CardBase, table=True):
player: Player = Relationship(back_populates='cards') player: Player = Relationship(back_populates='cards')
team: Team = Relationship(back_populates='cards',) team: Team = Relationship(back_populates='cards')
lineups: list['Lineup'] = Relationship(back_populates='card', cascade_delete=True) 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): class Lineup(SQLModel, table=True):
@ -655,6 +892,10 @@ class Lineup(SQLModel, table=True):
card_id: int = Field(foreign_key='card.id', index=True, ondelete='CASCADE') card_id: int = Field(foreign_key='card.id', index=True, ondelete='CASCADE')
card: Card = Relationship(back_populates='lineups') 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 # TODO: add function to return string value of game stats
@ -725,6 +966,14 @@ class PlayBase(SQLModel):
is_new_inning: bool = Field(default=False) is_new_inning: bool = Field(default=False)
managerai_id: int | None = Field(default=None, foreign_key='managerai.id') 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): def ai_run_diff(self):
if self.game.ai_team == 'away': if self.game.ai_team == 'away':
return self.away_score - self.home_score return self.away_score - self.home_score
@ -798,26 +1047,36 @@ class Play(PlayBase, table=True):
ai_note = '' ai_note = ''
# Holding Baserunners # Holding Baserunners
if self.starting_outs == 2 and self.on_base_code > 0: if self.starting_outs == 2 and self.on_base_code > 0:
if self.on_base_code in [1, 2]: if self.on_base_code == 1:
ai_note += f'- hold the runner\n' ai_note += f'- hold {self.on_first.player.name}\n'
elif self.on_base_code in [4, 7]: elif self.on_base_code == 2:
ai_note += f'- hold the runners\n' ai_note += f'- hold {self.on_second.player.name}\n'
elif self.on_base_code == 5: elif self.on_base_code in [4, 5, 7]:
ai_note += f'- hold the runner on first\n' 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: elif self.on_base_code == 6:
ai_note += f'- hold the runner on second\n' ai_note += f'- hold {self.on_second.player.name} on 2nd\n'
elif self.on_base_code in [1, 5]: elif self.on_base_code in [1, 5]:
ai_note += f'- hold the runner on 1st if they have ***** auto-jump\n' 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]: elif self.on_base_code in [2, 4]:
ai_note += f'- hold the runner on 2nd if safe range is 14+\n' 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 # Defensive Alignment
if self.on_third and self.starting_outs < 2: if self.on_third and self.starting_outs < 2:
if abs(self.away_score - self.home_score) <= 3: 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' ai_note += f'- play the whole infield in\n'
else: else:
ai_note += f'- play the corners in\n' 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 return ai_note
@property @property
@ -844,11 +1103,16 @@ class Play(PlayBase, table=True):
if self.game.ai_team is None: if self.game.ai_team is None:
return False return False
if (self.game.ai_team.lower() == 'away' and self.inning_half.lower() == 'top') or (self.game.ai_team.lower() == 'home' and self.inning_half.lower() == 'bot'): if (self.game.ai_team == 'away' and self.inning_half == 'top') or (self.game.ai_team == 'home' and self.inning_half == 'bot'):
return True return True
else: else:
return False return False
@property
def could_walkoff(self) -> bool:
return False
""" """
BEGIN DEVELOPMENT HELPERS BEGIN DEVELOPMENT HELPERS
""" """

View File

@ -1,12 +1,13 @@
import datetime import datetime
import logging import logging
import math import math
from typing import Literal
import pydantic import pydantic
from sqlalchemy import func from sqlalchemy import func
from api_calls import db_get, db_post from api_calls import db_get, db_post
from in_game.gameplay_models import CACHE_LIMIT, Card, CardBase, Lineup, Player, PlayerBase, Session, Team, TeamBase, select, or_, Game, Play from in_game.gameplay_models import CACHE_LIMIT, BatterScouting, BatterScoutingBase, BattingCard, BattingCardBase, BattingRatings, BattingRatingsBase, Card, CardBase, Lineup, PitcherScouting, PitchingCard, PitchingCardBase, PitchingRatings, PitchingRatingsBase, Player, PlayerBase, PositionRating, PositionRatingBase, Session, Team, TeamBase, select, or_, Game, Play
from exceptions import DatabaseError, log_exception, PlayNotFoundException from exceptions import DatabaseError, PositionNotFoundException, log_exception, PlayNotFoundException
logger = logging.getLogger('discord_app') logger = logging.getLogger('discord_app')
@ -147,6 +148,134 @@ async def get_player_or_none(session: Session, player_id: int, skip_cache: bool
return None return None
async def get_batter_scouting_or_none(session: Session, card: Card, skip_cache: bool = False) -> BatterScouting | None:
logger.info(f'Getting batting scouting for card ID #{card.id}: {card.player.name_with_desc}')
if not skip_cache and card.batterscouting is not None:
this_scouting = session.get(BatterScouting, card.batterscouting.id)
if this_scouting is not None:
logger.info(f'we found a cached scouting: {this_scouting} / created {this_scouting.created}')
tdelta = datetime.datetime.now() - this_scouting.created
logger.debug(f'tdelta: {tdelta}')
if tdelta.total_seconds() < CACHE_LIMIT:
return this_scouting
else:
session.delete(this_scouting)
session.commit()
def cache_scouting(batting_card: dict, ratings_vr: dict, ratings_vl: dict) -> BatterScouting:
valid_bc = BattingCardBase.model_validate(batting_card, from_attributes=True)
db_bc = BattingCard.model_validate(valid_bc)
valid_vl = BattingRatingsBase.model_validate(ratings_vl, from_attributes=True)
db_vl = BattingRatings.model_validate(valid_vl)
valid_vr = BattingRatingsBase.model_validate(ratings_vr, from_attributes=True)
db_vr = BattingRatings.model_validate(valid_vr)
db_scouting = BatterScouting(
battingcard=db_bc,
ratings_vl=db_vl,
ratings_vr=db_vr
)
session.add(db_scouting)
session.commit()
session.refresh(db_scouting)
return db_scouting
s_query = await db_get(f'battingcardratings/player/{card.player.id}', none_okay=False)
if s_query['count'] == 2:
return cache_scouting(
batting_card=s_query['ratings'][0]['battingcard'],
ratings_vr=s_query['ratings'][0] if s_query['ratings'][0]['vs_hand'] == 'R' else s_query['ratings'][1],
ratings_vl=s_query['ratings'][0] if s_query['ratings'][0]['vs_hand'] == 'L' else s_query['ratings'][1]
)
return None
async def get_pitcher_scouting_or_none(session: Session, card: Card, skip_cache: bool = False) -> PitcherScouting | None:
logger.info(f'Getting pitching scouting for card ID #{card.id}: {card.player.name_with_desc}')
if not skip_cache and card.pitcherscouting is not None:
this_scouting = session.get(PitcherScouting, card.pitcherscouting.id)
if this_scouting is not None:
logger.info(f'we found a cached scouting: {this_scouting} / created {this_scouting.created}')
tdelta = datetime.datetime.now() - this_scouting.created
logger.debug(f'tdelta: {tdelta}')
if tdelta.total_seconds() < CACHE_LIMIT:
return this_scouting
else:
session.delete(this_scouting)
session.commit()
def cache_scouting(pitching_card: dict, ratings_vr: dict, ratings_vl: dict) -> PitcherScouting:
valid_bc = PitchingCardBase.model_validate(pitching_card, from_attributes=True)
db_bc = PitchingCard.model_validate(valid_bc)
valid_vl = PitchingRatingsBase.model_validate(ratings_vl, from_attributes=True)
db_vl = PitchingRatings.model_validate(valid_vl)
valid_vr = PitchingRatingsBase.model_validate(ratings_vr, from_attributes=True)
db_vr = PitchingRatings.model_validate(valid_vr)
db_scouting = PitcherScouting(
pitchingcard=db_bc,
ratings_vl=db_vl,
ratings_vr=db_vr
)
session.add(db_scouting)
session.commit()
session.refresh(db_scouting)
return db_scouting
s_query = await db_get(f'pitchingcardratings/player/{card.player.id}', none_okay=False)
if s_query['count'] == 2:
scouting = cache_scouting(
pitching_card=s_query['ratings'][0]['pitchingcard'],
ratings_vr=s_query['ratings'][0] if s_query['ratings'][0]['vs_hand'] == 'R' else s_query['ratings'][1],
ratings_vl=s_query['ratings'][0] if s_query['ratings'][0]['vs_hand'] == 'L' else s_query['ratings'][1]
)
pos_rating = await get_and_cache_position(session, card, 'P')
return scouting
return None
# async def get_position_rating_or_none(session: Session, card: Card, position: str, skip_cache: bool = False) -> PositionRating | None:
# logger.info(f'Getting position rating for card ID')
# if not skip_cache:
# ratings = session.exec(select(PositionRating).where(PositionRating.player == card.player, PositionRating.variant == card.variant, PositionRating.position == position).limit(1)).all()
# """Test all of this; rebuild DB"""
# if len(ratings) > 0:
# logger.info(f'we found a cached position: {ratings[0]} / created {ratings[0].created}')
# tdelta = datetime.datetime.now() - ratings[0].created
# logger.debug(f'tdelta: {tdelta}')
# if tdelta.total_seconds() < CACHE_LIMIT:
# return ratings[0]
# else:
# session.delete(ratings[0])
# session.commit()
# def cache_rating(json_data: dict) -> PositionRating:
# valid_position = PositionRatingBase.model_validate(json_data, from_attributes=True)
# db_position = PositionRating.model_validate(valid_position)
# session.add(db_position)
# session.commit()
# session.refresh(db_position)
# return db_position
# p_query = await db_get('cardpositions', params=[('player_id', card.player.id)])
# if p_query['count'] > 0:
# return cache_rating(p_query['positions'][0])
# return None
def get_player_id_from_dict(json_data: dict) -> int: def get_player_id_from_dict(json_data: dict) -> int:
logger.info(f'Getting player from dict {json_data}') logger.info(f'Getting player from dict {json_data}')
if 'player_id' in json_data: if 'player_id' in json_data:
@ -165,6 +294,60 @@ def get_player_name_from_dict(json_data: dict) -> str:
log_exception(KeyError, 'Player name could not be extracted from json data') log_exception(KeyError, 'Player name could not be extracted from json data')
async def shared_get_scouting(session: Session, this_card: Card, which: Literal['batter', 'pitcher']):
if which == 'batter':
logger.info(f'Pulling batter scouting for {this_card.player.name_with_desc}')
this_scouting = await get_batter_scouting_or_none(session, this_card)
else:
logger.info(f'Pulling pitcher scouting for {this_card.player.name_with_desc}')
this_scouting = await get_pitcher_scouting_or_none(session, this_card)
logger.info(f'this_scouting: {this_scouting}')
return this_scouting
async def get_and_cache_position(session: Session, this_card: Card, position: Literal['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF'], skip_cache: bool = False):
logger.info(f'Pulling position rating for {this_card.player.name_with_desc} at {position}')
if not skip_cache:
this_pos = session.exec(select(PositionRating).where(PositionRating.player_id == this_card.player.id, PositionRating.position == position, PositionRating.variant == this_card.variant)).all()
logger.info(f'Ratings found: {len(this_pos)}')
if len(this_pos) > 0:
logger.info(f'we found a cached position rating: {this_pos[0]} / created: {this_pos[0].created}')
tdelta = datetime.datetime.now() - this_pos[0].created
logger.debug(f'tdelta: {tdelta}')
if tdelta.total_seconds() < CACHE_LIMIT:
return this_pos[0]
else:
session.delete(this_pos[0])
session.commit()
def cache_pos(json_data: dict) -> PositionRating:
if 'id' in json_data:
del json_data['id']
valid_pos = PositionRatingBase.model_validate(json_data, from_attributes=True)
db_pos = PositionRating.model_validate(valid_pos)
session.add(db_pos)
session.commit()
session.refresh(db_pos)
return db_pos
p_query = await db_get('cardpositions', params=[('player_id', this_card.player.id), ('position', position)])
if p_query['count'] > 0:
json_data = p_query['positions'][0]
json_data['player_id'] = get_player_id_from_dict(json_data['player'])
this_pos = cache_pos(json_data)
session.add(this_pos)
session.commit()
session.refresh(this_pos)
return this_card
log_exception(PositionNotFoundException, f'{position} ratings not found for {this_card.player.name_with_desc}')
async def get_or_create_ai_card(session: Session, player: Player, team: Team, skip_cache: bool = False, dev_mode: bool = False) -> Card: async def get_or_create_ai_card(session: Session, player: Player, team: Team, skip_cache: bool = False, dev_mode: bool = False) -> Card:
logger.info(f'Getting or creating card for {player.name_with_desc} on the {team.sname}') logger.info(f'Getting or creating card for {player.name_with_desc} on the {team.sname}')
if not team.is_ai: if not team.is_ai:
@ -205,6 +388,15 @@ async def get_or_create_ai_card(session: Session, player: Player, team: Team, sk
this_card = await pull_card(player, team) this_card = await pull_card(player, team)
if this_card is not None: if this_card is not None:
if player.pos_1 not in ['SP', 'RP']:
this_card.batterscouting = await shared_get_scouting(session, this_card, 'batter')
else:
this_card.pitcherscouting = await shared_get_scouting(session, this_card, 'pitcher')
session.add(this_card)
session.commit()
session.refresh(this_card)
return this_card return this_card
logger.info(f'gameplay_models - get_or_create_ai_card: creating {player.description} {player.name} card for {team.abbrev}') logger.info(f'gameplay_models - get_or_create_ai_card: creating {player.description} {player.name} card for {team.abbrev}')
@ -225,6 +417,15 @@ async def get_or_create_ai_card(session: Session, player: Player, team: Team, sk
this_card = await pull_card(player, team) this_card = await pull_card(player, team)
if this_card is not None: if this_card is not None:
if player.pos_1 not in ['SP', 'RP']:
this_card.batterscouting = await shared_get_scouting(session, this_card, 'batter')
else:
this_card.pitcherscouting = await shared_get_scouting(session, this_card, 'pitcher')
session.add(this_card)
session.commit()
session.refresh(this_card)
return this_card return this_card
err = f'Could not create {player.name} card for {team.abbrev}' err = f'Could not create {player.name} card for {team.abbrev}'
@ -268,8 +469,20 @@ async def get_card_or_none(session: Session, card_id: int, skip_cache: bool = Fa
if this_team is None: if this_team is None:
raise LookupError(f'Team ID {c_query["team_id"]} not found during card check') raise LookupError(f'Team ID {c_query["team_id"]} not found during card check')
return cache_card(c_query) logger.info(f'Caching card ID {card_id} now')
this_card = cache_card(c_query)
if this_player.pos_1 not in ['SP', 'RP']:
this_card.batterscouting = await shared_get_scouting(session, this_card, 'batter')
else:
this_card.pitcherscouting = await shared_get_scouting(session, this_card, 'pitcher')
session.add(this_card)
session.commit()
session.refresh(this_card)
return this_card
return None return None

View File

@ -26,4 +26,12 @@ class ThrowResponse(pydantic.BaseModel):
at_trail_runner: bool = False # Stops on False at_trail_runner: bool = False # Stops on False
trail_max_safe: int = 10 trail_max_safe: int = 10
trail_max_safe_delta: int = -6 trail_max_safe_delta: int = -6
class DefenseResponse(pydantic.BaseModel):
hold_first: bool = False
hold_second: bool = False
hold_third: bool = False
outfield_in: bool = False
infield_in: bool = False
corners_in: bool = False

View File

@ -20,7 +20,7 @@ def get_result(pitcher: data_cache.PitchingWrapper, batter: data_cache.BattingWr
if which == 'pitcher': if which == 'pitcher':
logger.info(f'in_game.simulations - get_result - grabbing pitcher card chances') logger.info(f'in_game.simulations - get_result - grabbing pitcher card chances')
ch_data = pitcher.ratings_vl if bat_hand.upper() == 'L' else pitcher.ratings_vr ch_data = pitcher.ratings_vl if bat_hand == 'L' else pitcher.ratings_vr
logger.info(f'ch_data: {ch_data}') logger.info(f'ch_data: {ch_data}')
# for field in fields(ch_data): # for field in fields(ch_data):
# if field.name not in unused_fields: # if field.name not in unused_fields:
@ -28,7 +28,7 @@ def get_result(pitcher: data_cache.PitchingWrapper, batter: data_cache.BattingWr
# ch_probs.append(getattr(data_cache.PitchingRatings, field.name)) # ch_probs.append(getattr(data_cache.PitchingRatings, field.name))
else: else:
logger.info(f'in_game.simulations - get_result - grabbing batter card chances') logger.info(f'in_game.simulations - get_result - grabbing batter card chances')
ch_data = batter.ratings_vl if pit_hand.upper() == 'L' else batter.ratings_vr ch_data = batter.ratings_vl if pit_hand == 'L' else batter.ratings_vr
logger.info(f'ch_data: {ch_data}') logger.info(f'ch_data: {ch_data}')
# for field in fields(ch_data): # for field in fields(ch_data):
# if field.name not in unused_fields: # if field.name not in unused_fields:

View File

@ -4,7 +4,7 @@ from sqlmodel import Session, SQLModel, create_engine
from sqlmodel.pool import StaticPool from sqlmodel.pool import StaticPool
from typing import Literal from typing import Literal
from in_game.gameplay_models import Card, Cardset, Game, GameCardsetLink, Lineup, ManagerAi, Play, Team, Player from in_game.gameplay_models import BatterScouting, BattingCard, BattingRatings, Card, Cardset, Game, GameCardsetLink, Lineup, ManagerAi, PitcherScouting, PitchingCard, PitchingRatings, Play, Team, Player
@pytest.fixture(name='session') @pytest.fixture(name='session')
@ -70,6 +70,12 @@ def session_fixture():
all_players = [] all_players = []
all_cards = [] all_cards = []
all_batscouting = []
all_pitscouting = []
all_pitratings = []
all_batratings = []
all_batcards = []
all_pitcards = []
pos_list = ['C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'DH', 'P'] pos_list = ['C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'DH', 'P']
for x in range(40): for x in range(40):
if x < 10: if x < 10:
@ -98,10 +104,58 @@ def session_fixture():
pos_1=pos_list[(x % 10)], pos_1=pos_list[(x % 10)],
description="Live" if x % 2 == 1 else "2024" description="Live" if x % 2 == 1 else "2024"
)) ))
all_cards.append(Card( # Is Batter
player_id=x+1, if x % 10 == 9:
team_id=team_id all_batcards.append(BattingCard(
)) id=x+1,
steal_high=10 + (x % 10),
hand='R'
))
all_batratings.append(BattingRatings(
id=x+1,
homerun=x % 10
))
all_batratings.append(BattingRatings(
id=x+1001,
homerun=x % 10
))
all_batscouting.append(BatterScouting(
id=x+1,
battingcard_id=x+1,
ratings_vr_id=x+1,
ratings_vl_id=x+1001,
))
all_cards.append(Card(
player_id=x+1,
team_id=team_id,
batterscouting_id=x+1
))
# Is Pitcher
else:
all_pitcards.append(PitchingCard(
id=x+1,
wild_pitch=x / 10,
hand='R'
))
all_pitratings.append(PitchingRatings(
id=x+1,
homerun=x % 10
))
all_pitratings.append(PitchingRatings(
id=x+1001,
homerun=x % 10
))
all_pitscouting.append(PitcherScouting(
id=x+1,
pitchingcard_id=x+1,
ratings_vr_id=x+1,
ratings_vl_id=x+1001
))
all_cards.append(Card(
player_id=x+1,
team_id=team_id,
pitcherscouting_id=x+1
))
all_players.append(Player( all_players.append(Player(
id=69, name='Player 68', cost=69*3, mlbclub='Junior All-Stars', franchise='Junior All-Stars', cardset=cardset_1, set_num=69, pos_1='DH', description='Live', created=datetime.datetime.today() - datetime.timedelta(days=60), image='player_69_image_URL', rarity_id=1 id=69, name='Player 68', cost=69*3, mlbclub='Junior All-Stars', franchise='Junior All-Stars', cardset=cardset_1, set_num=69, pos_1='DH', description='Live', created=datetime.datetime.today() - datetime.timedelta(days=60), image='player_69_image_URL', rarity_id=1

View File

@ -0,0 +1,226 @@
import pytest
from sqlmodel import Session, select, func
from in_game.gameplay_models import Card
from in_game.gameplay_queries import get_batter_scouting_or_none, get_card_or_none, get_pitcher_scouting_or_none
from tests.factory import session_fixture
sample_ratings_query = {
"count": 2,
"ratings": [
{
"id": 7673,
"battingcard": {
"id": 3837,
"player": {
"player_id": 395,
"p_name": "Cedric Mullins",
"cost": 256,
"image": "https://pd.manticorum.com/api/v2/players/395/battingcard?d=2023-11-19",
"image2": None,
"mlbclub": "Baltimore Orioles",
"franchise": "Baltimore Orioles",
"cardset": {
"id": 1,
"name": "2021 Season",
"description": "Cards based on the full 2021 season",
"event": None,
"for_purchase": True,
"total_cards": 791,
"in_packs": True,
"ranked_legal": False
},
"set_num": 395,
"rarity": {
"id": 2,
"value": 3,
"name": "All-Star",
"color": "FFD700"
},
"pos_1": "CF",
"pos_2": None,
"pos_3": None,
"pos_4": None,
"pos_5": None,
"pos_6": None,
"pos_7": None,
"pos_8": None,
"headshot": "https://www.baseball-reference.com/req/202206291/images/headshots/2/24bf4355_mlbam.jpg",
"vanity_card": None,
"strat_code": "17929",
"bbref_id": "mullice01",
"fangr_id": None,
"description": "2021",
"quantity": 999,
"mlbplayer": {
"id": 396,
"first_name": "Cedric",
"last_name": "Mullins",
"key_fangraphs": 17929,
"key_bbref": "mullice01",
"key_retro": "mullc002",
"key_mlbam": 656775,
"offense_col": 1
}
},
"variant": 0,
"steal_low": 9,
"steal_high": 12,
"steal_auto": True,
"steal_jump": 0.2222222222222222,
"bunting": "C",
"hit_and_run": "B",
"running": 13,
"offense_col": 1,
"hand": "L"
},
"vs_hand": "L",
"pull_rate": 0.43888889,
"center_rate": 0.32777778,
"slap_rate": 0.23333333,
"homerun": 2.25,
"bp_homerun": 5.0,
"triple": 0.0,
"double_three": 0.0,
"double_two": 2.4,
"double_pull": 7.2,
"single_two": 4.6,
"single_one": 3.5,
"single_center": 5.85,
"bp_single": 5.0,
"hbp": 2.0,
"walk": 9.0,
"strikeout": 23.0,
"lineout": 3.0,
"popout": 6.0,
"flyout_a": 0.0,
"flyout_bq": 0.15,
"flyout_lf_b": 2.8,
"flyout_rf_b": 3.75,
"groundout_a": 0.0,
"groundout_b": 9.0,
"groundout_c": 13.5,
"avg": 0.2851851851851852,
"obp": 0.387037037037037,
"slg": 0.5060185185185185
},
{
"id": 7674,
"battingcard": {
"id": 3837,
"player": {
"player_id": 395,
"p_name": "Cedric Mullins",
"cost": 256,
"image": "https://pd.manticorum.com/api/v2/players/395/battingcard?d=2023-11-19",
"image2": None,
"mlbclub": "Baltimore Orioles",
"franchise": "Baltimore Orioles",
"cardset": {
"id": 1,
"name": "2021 Season",
"description": "Cards based on the full 2021 season",
"event": None,
"for_purchase": True,
"total_cards": 791,
"in_packs": True,
"ranked_legal": False
},
"set_num": 395,
"rarity": {
"id": 2,
"value": 3,
"name": "All-Star",
"color": "FFD700"
},
"pos_1": "CF",
"pos_2": None,
"pos_3": None,
"pos_4": None,
"pos_5": None,
"pos_6": None,
"pos_7": None,
"pos_8": None,
"headshot": "https://www.baseball-reference.com/req/202206291/images/headshots/2/24bf4355_mlbam.jpg",
"vanity_card": None,
"strat_code": "17929",
"bbref_id": "mullice01",
"fangr_id": None,
"description": "2021",
"quantity": 999,
"mlbplayer": {
"id": 396,
"first_name": "Cedric",
"last_name": "Mullins",
"key_fangraphs": 17929,
"key_bbref": "mullice01",
"key_retro": "mullc002",
"key_mlbam": 656775,
"offense_col": 1
}
},
"variant": 0,
"steal_low": 9,
"steal_high": 12,
"steal_auto": True,
"steal_jump": 0.2222222222222222,
"bunting": "C",
"hit_and_run": "B",
"running": 13,
"offense_col": 1,
"hand": "L"
},
"vs_hand": "R",
"pull_rate": 0.43377483,
"center_rate": 0.32119205,
"slap_rate": 0.24503311,
"homerun": 2.0,
"bp_homerun": 7.0,
"triple": 0.0,
"double_three": 0.0,
"double_two": 2.7,
"double_pull": 7.95,
"single_two": 3.3,
"single_one": 0.0,
"single_center": 8.6,
"bp_single": 5.0,
"hbp": 1.0,
"walk": 12.9,
"strikeout": 11.1,
"lineout": 8.0,
"popout": 11.0,
"flyout_a": 0.0,
"flyout_bq": 0.4,
"flyout_lf_b": 4.05,
"flyout_rf_b": 6.0,
"groundout_a": 0.0,
"groundout_b": 3.0,
"groundout_c": 14.0,
"avg": 0.2828703703703704,
"obp": 0.4115740740740741,
"slg": 0.5342592592592592
}
]
}
async def test_create_scouting(session: Session):
this_card = await get_card_or_none(session, card_id=1405)
assert this_card.player.id == 395
assert this_card.team.id == 31
assert this_card.batterscouting.battingcard_id == sample_ratings_query["ratings"][0]['battingcard']['id']
# this_scouting = await get_batter_scouting_or_none(session, this_card)
# assert this_scouting.battingcard_id == sample_ratings_query["ratings"][0]['battingcard']['id']
# this_card = await get_card_or_none(session, card_id=1406)
# assert this_card.player.id == 161
# assert this_card.team.id == 31
# this_scouting = await get_pitcher_scouting_or_none(session, this_card)
# assert this_scouting.pitchingcard_id == 4294

View File

@ -167,3 +167,9 @@ async def test_walks(session: Session):
assert this_play.on_base_code == 1 assert this_play.on_base_code == 1
def test_get_one(session: Session):
play_1 = session.exec(select(Play).where(Play.id == 1)).one()
assert play_1.play_num == 1