From 3d333dabc38ee61b9bd8c554cee50682e766876a Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Sat, 16 Nov 2024 00:31:54 -0600 Subject: [PATCH] Update api logging New Position exception Pull scouting data with lineups More bunt types String validation on gameplay models AI Defensive alignment --- api_calls.py | 21 +- cogs/gameplay.py | 55 +++- command_logic/logic_gameplay.py | 78 ++++- exceptions.py | 3 + in_game/ai_manager.py | 53 ++- in_game/gameplay_models.py | 308 ++++++++++++++++-- in_game/gameplay_queries.py | 221 ++++++++++++- in_game/managerai_responses.py | 10 +- in_game/simulations.py | 4 +- tests/factory.py | 64 +++- .../test_batterscouting_model.py | 226 +++++++++++++ tests/gameplay_models/test_play_model.py | 6 + 12 files changed, 956 insertions(+), 93 deletions(-) create mode 100644 tests/gameplay_models/test_batterscouting_model.py diff --git a/api_calls.py b/api_calls.py index 06e8799..5930de8 100644 --- a/api_calls.py +++ b/api_calls.py @@ -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): - 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') + start = 0 + end = 3000 + while end < 300000: + 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, - timeout: int = 3): +async def db_get(endpoint: str, api_ver: int = 2, object_id: int = None, params: list = None, none_okay: bool = True, timeout: int = 3): 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}' logger.info(log_string) if master_debug else logger.debug(log_string) diff --git a/cogs/gameplay.py b/cogs/gameplay.py index 3565fca..085c759 100644 --- a/cogs/gameplay.py +++ b/cogs/gameplay.py @@ -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.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_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 @@ -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): 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( 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?', @@ -211,6 +211,8 @@ class Gameplay(commands.Cog): ) return + await get_and_cache_position(session, human_sp_card, 'P') + legal_data = await legal_check([sp_card_id], difficulty_name=league.value) if not legal_data['legal']: await interaction.edit_original_response( @@ -259,6 +261,12 @@ class Gameplay(commands.Cog): # Commit game and lineups session.add(this_game) 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) 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)) + await interaction.edit_original_response(content='Heard from sheets, pulling in scouting data...') + for batter in human_lineups: session.add(batter) session.commit() + for batter in human_lineups: + if batter.position != 'DH': + await get_and_cache_position(session, batter.card, batter.position) + 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') @@ -397,8 +411,8 @@ class Gameplay(commands.Cog): with Session(engine) as session: 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}') + this_play = await flyballs(session, interaction, this_play, flyball_type) await self.complete_and_post_play( session, @@ -413,8 +427,8 @@ class Gameplay(commands.Cog): with Session(engine) as session: 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}') + 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) @@ -437,8 +451,8 @@ class Gameplay(commands.Cog): with Session(engine) as session: 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}') + 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) @@ -447,8 +461,8 @@ class Gameplay(commands.Cog): with Session(engine) as session: 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}') + this_play = await triples(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: 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}') + this_play = await homeruns(session, interaction, this_play, homerun_type) await self.complete_and_post_play(session, interaction, this_play) @@ -467,8 +481,8 @@ class Gameplay(commands.Cog): with Session(engine) as session: 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}') + this_play = await walks(session, interaction, this_play, walk_type) await self.complete_and_post_play(session, interaction, this_play) @@ -477,8 +491,8 @@ class Gameplay(commands.Cog): with Session(engine) as session: 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}') + this_play = await strikeouts(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: 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}') + this_play = await popouts(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: 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}') + this_play = await hit_by_pitch(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']): with Session(engine) as session: 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}') + this_play = await bunts(session, interaction, this_play, bunt_type) await self.complete_and_post_play(session, interaction, this_play) @@ -517,8 +542,8 @@ class Gameplay(commands.Cog): with Session(engine) as session: 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}') + this_play = undo_play(session, this_play) await self.post_play(session, interaction, this_play) @@ -528,8 +553,8 @@ class Gameplay(commands.Cog): with Session(engine) as session: 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}') + await show_defense_cards(session, interaction, this_play, position) async def setup(bot): diff --git a/command_logic/logic_gameplay.py b/command_logic/logic_gameplay.py index b88fce6..230049f 100644 --- a/command_logic/logic_gameplay.py +++ b/command_logic/logic_gameplay.py @@ -21,6 +21,16 @@ from utilities.pages import Pagination logger = logging.getLogger('discord_app') WPA_DF = pd.read_csv(f'storage/wpa_data.csv').set_index('index') +TO_BASE = { + 2: 'to second', + 3: 'to third', + 4: 'home' +} +AT_BASE = { + 2: 'at second', + 3: 'at third', + 4: 'at home' +} 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 obc = 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 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) logger.info(f'throw from {outfielder.player.name_with_desc}') 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 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']): this_play.ab = 1 if bunt_type != 'sacrifice' else 0 this_play.sac = 1 if bunt_type != 'sacrifice' else 0 + this_play.outs = 1 if bunt_type == 'sacrifice': this_play = advance_runners(session, this_play, num_bases=1) elif bunt_type == 'popout': this_play = advance_runners(session, this_play, num_bases=0) + elif bunt_type == 'bad': + this_play = advance_runners(session, this_play, num_bases=1) + this_play.batter_final = 1 + + if this_play.on_third is not None: + this_play.on_third_final = None + + elif this_play.on_second is not None: + this_play.on_second_final = None + + elif this_play.on_first is not None: + this_play.on_first_final = None + elif bunt_type == 'double-play': + this_play = advance_runners(session, this_play, num_bases=0) + this_play.outs = 2 if this_play.starting_outs < 2 else 1 + + if this_play.on_third is not None: + this_play.on_third_final = None + + elif this_play.on_second is not None: + this_play.on_second_final = None + + elif this_play.on_first is not None: + this_play.on_first_final = None + else: 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 ) + 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' hold_string = None diff --git a/exceptions.py b/exceptions.py index a0afdbd..f112cca 100644 --- a/exceptions.py +++ b/exceptions.py @@ -49,3 +49,6 @@ class PlayInitException(GameException): class DatabaseError(GameException): pass + +class PositionNotFoundException(GameException): + pass diff --git a/in_game/ai_manager.py b/in_game/ai_manager.py index d9cef19..370f429 100644 --- a/in_game/ai_manager.py +++ b/in_game/ai_manager.py @@ -12,8 +12,7 @@ from peewee import * from typing import Optional, Literal from in_game import data_cache -import in_game.gameplay_models as iggm -from in_game.gameplay_models import Play, Session, Game, Team +from in_game.gameplay_models import Play, Session, Game, Team, Lineup from in_game.gameplay_queries import get_or_create_ai_card, get_player_id_from_dict, get_player_or_none db = SqliteDatabase( @@ -42,28 +41,28 @@ class BaseModel(Model): database = db -class Lineup(BaseModel): - team_id = IntegerField() - game_level = CharField() # 'minor', 'major', 'hof' - vs_hand = CharField() # 'left', 'right' - bat_one_card = IntegerField() - bat_one_pos = CharField() - bat_two_card = IntegerField() - bat_two_pos = CharField() - bat_three_card = IntegerField() - bat_three_pos = CharField() - bat_four_card = IntegerField() - bat_four_pos = CharField() - bat_five_card = IntegerField() - bat_five_pos = CharField() - bat_six_card = IntegerField() - bat_six_pos = CharField() - bat_seven_card = IntegerField() - bat_seven_pos = CharField() - bat_eight_card = IntegerField() - bat_eight_pos = CharField() - bat_nine_card = IntegerField() - bat_nine_pos = CharField() +# class Lineup(BaseModel): +# team_id = IntegerField() +# game_level = CharField() # 'minor', 'major', 'hof' +# vs_hand = CharField() # 'left', 'right' +# bat_one_card = IntegerField() +# bat_one_pos = CharField() +# bat_two_card = IntegerField() +# bat_two_pos = CharField() +# bat_three_card = IntegerField() +# bat_three_pos = CharField() +# bat_four_card = IntegerField() +# bat_four_pos = CharField() +# bat_five_card = IntegerField() +# bat_five_pos = CharField() +# bat_six_card = IntegerField() +# bat_six_pos = CharField() +# bat_seven_card = IntegerField() +# bat_seven_pos = CharField() +# bat_eight_card = IntegerField() +# bat_eight_pos = CharField() +# bat_nine_card = IntegerField() +# bat_nine_pos = CharField() 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_card = await get_or_create_ai_card(session, player=this_player, team=team) - lineups.append(iggm.Lineup( + lineups.append(Lineup( position=y[0], batting_order=i, game=game, @@ -316,7 +315,7 @@ async def get_starting_lineup(session: Session, team: Team, game: Game, league_n 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) if is_home: 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)) sp_card = await get_or_create_ai_card(session, this_player, this_team) - return iggm.Lineup( + return Lineup( team=this_team, player=this_player, card=sp_card, diff --git a/in_game/gameplay_models.py b/in_game/gameplay_models.py index 809d440..90bc639 100644 --- a/in_game/gameplay_models.py +++ b/in_game/gameplay_models.py @@ -5,12 +5,12 @@ from typing import Literal import discord 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 api_calls import db_get, db_post 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') @@ -65,7 +65,7 @@ class TeamBase(SQLModel): ranking: int has_guide: 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 def description(self) -> str: @@ -128,6 +128,10 @@ class Game(SQLModel, table=True): ) 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: @@ -363,7 +367,7 @@ class ManagerAi(ManagerAiBase, table=True): this_resp = JumpResponse() this_play = this_game.current_play_or_none(session) 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 run_diff = this_play.away_score - this_play.home_score @@ -418,7 +422,7 @@ class ManagerAi(ManagerAiBase, table=True): this_resp = TagResponse() this_play = this_game.current_play_or_none(session) 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() 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_play = this_game.current_play_or_none(session) 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() 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_play = this_game.current_play_or_none(session) 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() 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 + 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) @@ -564,7 +609,14 @@ class PlayerBase(SQLModel): bbref_id: str | None = Field(default=None) fangr_id: str | 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 def p_card_url(self): @@ -597,6 +649,7 @@ 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): @@ -620,18 +673,202 @@ def player_description(player: Player = None, player_dict: dict = None) -> str: 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 | None = Field(default=datetime.datetime.now()) + 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',) + 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): @@ -655,6 +892,10 @@ class Lineup(SQLModel, table=True): 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 @@ -725,6 +966,14 @@ class PlayBase(SQLModel): 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 @@ -798,26 +1047,36 @@ class Play(PlayBase, table=True): ai_note = '' # Holding Baserunners if self.starting_outs == 2 and self.on_base_code > 0: - if self.on_base_code in [1, 2]: - ai_note += f'- hold the runner\n' - elif self.on_base_code in [4, 7]: - ai_note += f'- hold the runners\n' - elif self.on_base_code == 5: - ai_note += f'- hold the runner on first\n' + 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 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]: - 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]: - 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 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' 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 @@ -844,11 +1103,16 @@ class Play(PlayBase, table=True): if self.game.ai_team is None: 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 else: return False + @property + def could_walkoff(self) -> bool: + return False + + """ BEGIN DEVELOPMENT HELPERS """ diff --git a/in_game/gameplay_queries.py b/in_game/gameplay_queries.py index fb2202b..df8dcce 100644 --- a/in_game/gameplay_queries.py +++ b/in_game/gameplay_queries.py @@ -1,12 +1,13 @@ import datetime import logging import math +from typing import Literal import pydantic from sqlalchemy import func 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 exceptions import DatabaseError, log_exception, PlayNotFoundException +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, PositionNotFoundException, log_exception, PlayNotFoundException 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 +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: logger.info(f'Getting player from dict {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') +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: logger.info(f'Getting or creating card for {player.name_with_desc} on the {team.sname}') 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) 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 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) 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 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: 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 diff --git a/in_game/managerai_responses.py b/in_game/managerai_responses.py index 04c26ed..b2acca2 100644 --- a/in_game/managerai_responses.py +++ b/in_game/managerai_responses.py @@ -26,4 +26,12 @@ class ThrowResponse(pydantic.BaseModel): at_trail_runner: bool = False # Stops on False trail_max_safe: int = 10 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 diff --git a/in_game/simulations.py b/in_game/simulations.py index ec26f25..639fc24 100644 --- a/in_game/simulations.py +++ b/in_game/simulations.py @@ -20,7 +20,7 @@ def get_result(pitcher: data_cache.PitchingWrapper, batter: data_cache.BattingWr if which == 'pitcher': 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}') # for field in fields(ch_data): # 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)) else: 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}') # for field in fields(ch_data): # if field.name not in unused_fields: diff --git a/tests/factory.py b/tests/factory.py index 3400c40..85579ea 100644 --- a/tests/factory.py +++ b/tests/factory.py @@ -4,7 +4,7 @@ from sqlmodel import Session, SQLModel, create_engine from sqlmodel.pool import StaticPool 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') @@ -70,6 +70,12 @@ def session_fixture(): all_players = [] 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'] for x in range(40): if x < 10: @@ -98,10 +104,58 @@ def session_fixture(): pos_1=pos_list[(x % 10)], description="Live" if x % 2 == 1 else "2024" )) - all_cards.append(Card( - player_id=x+1, - team_id=team_id - )) + # Is Batter + if x % 10 == 9: + 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( 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 diff --git a/tests/gameplay_models/test_batterscouting_model.py b/tests/gameplay_models/test_batterscouting_model.py new file mode 100644 index 0000000..788b3b1 --- /dev/null +++ b/tests/gameplay_models/test_batterscouting_model.py @@ -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 + diff --git a/tests/gameplay_models/test_play_model.py b/tests/gameplay_models/test_play_model.py index 7690cd5..2f2c478 100644 --- a/tests/gameplay_models/test_play_model.py +++ b/tests/gameplay_models/test_play_model.py @@ -167,3 +167,9 @@ async def test_walks(session: Session): 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 + +