From 46860fb3c0f196d9ecebe3388ad28e400b1d5e41 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Mon, 14 Oct 2024 20:46:55 -0500 Subject: [PATCH] /read-lineup logic is complete --- cogs/gameplay.py | 99 ++++++++++++++++------ cogs/players.py | 2 +- command_logic/gameplay.py | 87 +++++++++++++++++++ in_game/ai_manager.py | 5 +- in_game/game_helpers.py | 4 + in_game/gameplay_models.py | 25 +++++- in_game/gameplay_queries.py | 16 +++- tests/gameplay_models/test_card_model.py | 3 +- tests/gameplay_models/test_lineup_model.py | 28 +++++- 9 files changed, 234 insertions(+), 35 deletions(-) create mode 100644 command_logic/gameplay.py diff --git a/cogs/gameplay.py b/cogs/gameplay.py index c3de1f2..20b5960 100644 --- a/cogs/gameplay.py +++ b/cogs/gameplay.py @@ -1,20 +1,20 @@ -import asyncio -import enum import logging from typing import Literal import discord from discord import app_commands from discord.app_commands import Choice -from discord.ext import commands +from discord.ext import commands, tasks +import pygsheets from api_calls import db_get +from command_logic.gameplay import get_lineups_from_sheets from helpers import PD_PLAYERS_ROLE_NAME, team_role, user_has_role, random_gif, random_from_list from in_game import ai_manager from in_game.game_helpers import PUBLIC_FIELDS_CATEGORY_NAME, legal_check -from in_game.gameplay_models import Lineup, Session, engine, get_card_or_none, player_description, select, Game, get_team_or_none -from in_game.gameplay_queries import get_channel_game_or_none, get_active_games_by_team +from in_game.gameplay_models import Lineup, 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 utilities.buttons import Confirm @@ -22,6 +22,19 @@ from utilities.buttons import Confirm class Gameplay(commands.Cog): def __init__(self, bot): self.bot = bot + self.sheets = None + + self.get_sheets.start() + + @tasks.loop(count=1) + async def get_sheets(self): + logging.info(f'Getting sheets') + self.sheets = pygsheets.authorize(service_file='storage/paper-dynasty-service-creds.json', retries=1) + + @get_sheets.before_loop + async def before_get_sheets(self): + logging.info(f'Waiting to get sheets') + await self.bot.wait_until_ready() async def cog_command_error(self, ctx, error): await ctx.send(f'{error}\n\nRun !help to see the command requirements') @@ -36,10 +49,10 @@ class Gameplay(commands.Cog): sp_card_id='Light gray number to the left of the pitcher\'s name on your depth chart' ) @app_commands.choices(league=[ - Choice(name='minor-league', value='Minor League'), - Choice(name='flashback', value='Flashback'), - Choice(name='major-league', value='Major League'), - Choice(name='hall-of-fame', value='Hall of Fame') + Choice(value='minor-league', name='Minor League'), + Choice(value='flashback', name='Flashback'), + Choice(value='major-league', name='Major League'), + Choice(value='hall-of-fame', name='Hall of Fame') ]) @app_commands.checks.has_any_role(PD_PLAYERS_ROLE_NAME) async def new_game_mlb_campaign_command( @@ -58,8 +71,7 @@ class Gameplay(commands.Cog): if interaction.channel.category is None or interaction.channel.category.name != PUBLIC_FIELDS_CATEGORY_NAME: await interaction.edit_original_response( - content=f'Why don\'t you head down to one of the Public Fields that way other humans can help if anything ' - f'pops up?' + content=f'Why don\'t you head down to one of the Public Fields that way other humans can help if anything pops up?' ) return @@ -101,19 +113,19 @@ class Gameplay(commands.Cog): def role_error(required_role: str, league_name: str, lower_league: str): return f'Ope. Looks like you haven\'t received the **{required_role}** role, yet!\n\nTo play **{league_name}** games, you need to defeat all 30 MLB teams in the {lower_league} campaign. You can see your progress with `/record`.\n\nIf you have completed the {lower_league} campaign, go ping Cal to get your new role!' - if league.name == 'flashback': + if league.value == 'flashback': if not user_has_role(interaction.user, 'PD - Major League'): await interaction.edit_original_response( content=role_error('PD - Major League', league_name='Flashback', lower_league='Minor League') ) return - elif league.name == 'major-league': + elif league.value == 'major-league': if not user_has_role(interaction.user, 'PD - Major League'): await interaction.edit_original_response( content=role_error('PD - Major League', league_name='Major League', lower_league='Minor League') ) return - elif league.name == 'hall-of-fame': + elif league.value == 'hall-of-fame': if not user_has_role(interaction.user, 'PD - Hall of Fame'): await interaction.edit_original_response( content=role_error('PD - Hall of Fame', league_name='Hall of Fame', lower_league='Major League') @@ -128,10 +140,10 @@ class Gameplay(commands.Cog): week_num=week_num, first_message=None if interaction.message is None else interaction.message.channel.id, ai_team='away' if away_team.is_ai else 'home', - game_type=league.name + game_type=league.value ) - game_info_log = f'{league.value} game between {away_team.description} and {home_team.description} / first message: {this_game.first_message}' + game_info_log = f'{league.name} game between {away_team.description} and {home_team.description} / first message: {this_game.first_message}' logging.info(game_info_log) # Get Human SP card @@ -149,10 +161,10 @@ class Gameplay(commands.Cog): ) return - legal_data = await legal_check([sp_card_id], difficulty_name=league.name) + legal_data = await legal_check([sp_card_id], difficulty_name=league.value) if not legal_data['legal']: await interaction.edit_original_response( - content=f'It looks like this is a Ranked Legal game and {player_description(player=human_sp_card.player)} is not legal in {league.value} games. You can start a new game once you pick a new SP.' + content=f'It looks like this is a Ranked Legal game and {player_description(player=human_sp_card.player)} is not legal in {league.name} games. You can start a new game once you pick a new SP.' ) return @@ -176,7 +188,7 @@ class Gameplay(commands.Cog): ai_team, this_game, True if home_team.is_ai else False, - league.name + league.value ) await interaction.edit_original_response( content=f'The {ai_team.sname} are starting **{player_description(player=ai_sp_lineup.player)}**:\n\n{ai_sp_lineup.player.p_card_url}' @@ -190,7 +202,7 @@ class Gameplay(commands.Cog): session, team=ai_team, game=this_game, - league_name=league.name, + league_name=league.value, sp_name=human_sp_card.player.name ) @@ -248,17 +260,17 @@ class Gameplay(commands.Cog): ) @app_commands.choices( roster=[ - Choice(name=1, value='Primary'), - Choice(name=2, value='Secondary'), - Choice(name=3, value='Ranked') + Choice(value='1', name='Primary'), + Choice(value='2', name='Secondary'), + Choice(value='3', name='Ranked') ], lineup=[ - Choice(name=1, value='v Right'), - Choice(name=2, value='v Left') + Choice(value='1', name='v Right'), + Choice(value='2', name='v Left') ] ) @app_commands.checks.has_any_role(PD_PLAYERS_ROLE_NAME) - async def read_lineup_command(self, interaction: discord.Interaction, roster: Choice[int], lineup: Choice[int]): + async def read_lineup_command(self, interaction: discord.Interaction, roster: Choice[str], lineup: Choice[str]): await interaction.response.defer() with Session(engine) as session: @@ -268,7 +280,42 @@ class Gameplay(commands.Cog): content=f'Hm. I don\'t see a game going on in this channel. Am I drunk?' ) return + + lineup_team = this_game.away_team if this_game.ai_team == 'home' else this_game.home_team + if interaction.user.id != lineup_team.gmid: + logging.info(f'{interaction.user.name} tried to run a command in Game {this_game.id} when they aren\'t a GM in the game.') + await interaction.edit_original_response(content='Bruh. Only GMs of the active teams can pull lineups.') + return + + existing_lineups = get_game_lineups( + session=session, + this_game=this_game, + specific_team=lineup_team, + is_active=True + ) + if len(existing_lineups) > 1: + await interaction.edit_original_response( + f'It looks like the {lineup_team.sname} already have a lineup. Run `/substitution` to make changes.' + ) + return + + await interaction.edit_original_response(content='Okay, let\'s put this lineup card together...') + if this_game.away_team == lineup_team: + this_game.away_roster_id = int(roster.value) + else: + this_game.home_roster_id = int(roster.value) + + session.add(this_game) + + 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)) + + for batter in human_lineups: + session.add(batter) + + session.commit() + + await interaction.edit_original_response(content=f'Lineups are read and logged!') async def setup(bot): diff --git a/cogs/players.py b/cogs/players.py index d6be12d..310c6f1 100644 --- a/cogs/players.py +++ b/cogs/players.py @@ -294,7 +294,7 @@ def get_record_embed(team: dict, results: dict, league: str): class Players(commands.Cog): def __init__(self, bot): self.bot = bot - self.sheets = pygsheets.authorize(service_file='storage/paper-dynasty-service-creds.json', retries=1) + # self.sheets = pygsheets.authorize(service_file='storage/paper-dynasty-service-creds.json', retries=1) self.player_list = [] self.cardset_list = [] self.freeze = False diff --git a/command_logic/gameplay.py b/command_logic/gameplay.py new file mode 100644 index 0000000..8e1a983 --- /dev/null +++ b/command_logic/gameplay.py @@ -0,0 +1,87 @@ + +import logging +from sqlmodel import Session, select + +from in_game.game_helpers import legal_check, CardLegalityException +from in_game.gameplay_models import Game, Lineup, Team +from in_game.gameplay_queries import get_card_or_none + + +async def get_lineups_from_sheets(session: Session, sheets, this_game: Game, this_team: Team, lineup_num: int, roster_num: int) -> list[Lineup]: + logging.debug(f'sheets: {sheets}') + + this_sheet = sheets.open_by_key(this_team.gsheet) + logging.debug(f'this_sheet: {this_sheet}') + + r_sheet = this_sheet.worksheet_by_title('My Rosters') + logging.debug(f'r_sheet: {r_sheet}') + + if lineup_num == 1: + row_start = 9 + row_end = 17 + else: + row_start = 18 + row_end = 26 + + if roster_num == 1: + l_range = f'H{row_start}:I{row_end}' + elif roster_num == 2: + l_range = f'J{row_start}:K{row_end}' + else: + l_range = f'L{row_start}:M{row_end}' + + logging.debug(f'l_range: {l_range}') + raw_cells = r_sheet.range(l_range) + logging.debug(f'raw_cells: {raw_cells}') + + try: + lineup_cells = [(row[0].value, int(row[1].value)) for row in raw_cells] + logging.debug(f'lineup_cells: {lineup_cells}') + except ValueError as e: + logging.error(f'Could not pull roster for {this_team.abbrev}: {e}') + raise ValueError(f'Uh oh. Looks like your roster might not be saved. I am reading blanks when I try to get the card IDs') + + all_lineups = [] + all_pos = [] + card_ids = [] + for index, row in enumerate(lineup_cells): + if '' in row: + break + + if row[0].upper() not in all_pos: + all_pos.append(row[0].upper()) + else: + raise SyntaxError(f'You have more than one {row[0].upper()} in this lineup. Please update and set the lineup again.') + + this_card = await get_card_or_none(session, card_id=int(row[1])) + if this_card is None: + raise LookupError( + f'Your {row[0].upper()} has a Card ID of {int(row[1])} and I cannot find that card. Did you sell it by chance? Or maybe you sold a duplicate and the bot sold the one you were using?' + ) + if this_card.team_id != this_team.id: + raise SyntaxError(f'Easy there, champ. Looks like card ID {row[1]} belongs to the {this_card.team.lname}. Try again with only cards you own.') + card_id = row[1] + card_ids.append(str(card_id)) + + this_lineup = Lineup( + position=row[0].upper(), + batting_order=index + 1, + game=this_game, + team=this_team, + player=this_card.player, + card=this_card + ) + all_lineups.append(this_lineup) + + legal_data = await legal_check([card_ids], difficulty_name=this_game.game_type) + logging.debug(f'legal_data: {legal_data}') + if not legal_data['legal']: + raise CardLegalityException(f'The following cards appear to be illegal for this game mode:\n{legal_data["error_string"]}') + + if len(all_lineups) != 9: + raise Exception(f'I was only able to pull in {len(all_lineups)} batters from Sheets. Please check your saved lineup and try again.') + + return all_lineups + + + \ No newline at end of file diff --git a/in_game/ai_manager.py b/in_game/ai_manager.py index 7883999..4e0d2f8 100644 --- a/in_game/ai_manager.py +++ b/in_game/ai_manager.py @@ -13,7 +13,8 @@ from typing import Optional, Literal from in_game import data_cache import in_game.gameplay_models as iggm -from in_game.gameplay_models import Session, Game, Team, get_or_create_ai_card, get_player_id_from_dict, get_player_or_none +from in_game.gameplay_models import Session, Game, Team +from in_game.gameplay_queries import get_or_create_ai_card, get_player_id_from_dict, get_player_or_none db = SqliteDatabase( 'storage/ai-database.db', @@ -352,7 +353,7 @@ async def get_starting_pitcher( f'teams/{this_team.id}/sp/{league_name}?sp_rank={sp_rank}{this_game.cardset_param_string}' ) this_player = await get_player_or_none(session, get_player_id_from_dict(sp_query)) - sp_card = await iggm.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( team=this_team, diff --git a/in_game/game_helpers.py b/in_game/game_helpers.py index fa5db1f..4568930 100644 --- a/in_game/game_helpers.py +++ b/in_game/game_helpers.py @@ -14,6 +14,10 @@ from typing import Literal, Optional PUBLIC_FIELDS_CATEGORY_NAME = 'Public Fields' +class CardLegalityException(Exception): + pass + + def single_onestar(this_play: StratPlay, comp_play: bool = True): patch_play(this_play.id, locked=True) advance_runners(this_play.id, num_bases=1) diff --git a/in_game/gameplay_models.py b/in_game/gameplay_models.py index 4ed5564..58742a1 100644 --- a/in_game/gameplay_models.py +++ b/in_game/gameplay_models.py @@ -50,12 +50,15 @@ class TeamBase(SQLModel): class Team(TeamBase, table=True): cards: list['Card'] = Relationship(back_populates='team', cascade_delete=True) lineups: list['Lineup'] = Relationship(back_populates='team', cascade_delete=True) + # away_games: list['Game'] = Relationship(back_populates='away_team') + # home_games: list['Game'] = Relationship(back_populates='home_team') + class Game(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) - away_team_id: int - home_team_id: int + away_team_id: int = Field(foreign_key='team.id') + home_team_id: int = Field(foreign_key='team.id') channel_id: int = Field(index=True) season: int active: bool | None = Field(default=True) @@ -71,6 +74,24 @@ class Game(SQLModel, table=True): game_type: str | None = Field(default=None) cardset_links: list[GameCardsetLink] = Relationship(back_populates='game', cascade_delete=True) + away_team: Team = Relationship( + # back_populates='away_games', + # sa_relationship_kwargs={ + # 'primaryjoin': 'Game.away_team_id==Team.id', + # 'foreign_keys': '[Game.away_team.id]', + # 'lazy': 'joined' + # } + sa_relationship_kwargs=dict(foreign_keys="[Game.away_team_id]") + ) + home_team: Team = Relationship( + # back_populates='home_games', + # sa_relationship_kwargs={ + # 'primaryjoin': 'Game.home_team_id==Team.id', + # 'foreign_keys': '[Game.home_team.id]', + # 'lazy': 'joined' + # } + sa_relationship_kwargs=dict(foreign_keys="[Game.home_team_id]") + ) lineups: list['Lineup'] = Relationship(back_populates='game', cascade_delete=True) @property diff --git a/in_game/gameplay_queries.py b/in_game/gameplay_queries.py index 279581b..9d22fca 100644 --- a/in_game/gameplay_queries.py +++ b/in_game/gameplay_queries.py @@ -3,7 +3,7 @@ import logging from sqlalchemy import func from api_calls import db_get, db_post -from in_game.gameplay_models import CACHE_LIMIT, Card, CardBase, Player, PlayerBase, Session, Team, TeamBase, select, or_, Game +from in_game.gameplay_models import CACHE_LIMIT, Card, CardBase, Lineup, Player, PlayerBase, Session, Team, TeamBase, select, or_, Game def get_games_by_channel(session: Session, channel_id: int) -> list[Game]: @@ -15,7 +15,7 @@ def get_channel_game_or_none(session: Session, channel_id: int) -> Game | None: if len(all_games) > 1: err = 'Too many games found in get_channel_game_or_none' logging.error(f'cogs.gameplay - get_channel_game_or_none - channel_id: {channel_id} / {err}') - raise LookupError(err) + raise Exception(err) elif len(all_games) == 0: return None return all_games[0] @@ -232,3 +232,15 @@ async def get_card_or_none(session: Session, card_id: int, skip_cache: bool = Fa return cache_card(c_query) return None + + +def get_game_lineups(session: Session, this_game: Game, specific_team: Team = None, is_active: bool = None) -> list[Lineup]: + st = select(Lineup).where(Lineup.game == this_game) + + if specific_team is not None: + st = st.where(Lineup.team == specific_team) + if is_active is not None: + st = st.where(Lineup.active == is_active) + + return session.exec(st).all() + diff --git a/tests/gameplay_models/test_card_model.py b/tests/gameplay_models/test_card_model.py index ca53575..57bcdc0 100644 --- a/tests/gameplay_models/test_card_model.py +++ b/tests/gameplay_models/test_card_model.py @@ -1,7 +1,8 @@ import datetime from sqlmodel import Session -from in_game.gameplay_models import CACHE_LIMIT, Card, Player, Team, select, get_card_or_none, get_or_create_ai_card +from in_game.gameplay_models import CACHE_LIMIT, Card, Player, Team, select, get_card_or_none +from in_game.gameplay_queries import get_or_create_ai_card from factory import session_fixture diff --git a/tests/gameplay_models/test_lineup_model.py b/tests/gameplay_models/test_lineup_model.py index 1e63385..ce5fa4c 100644 --- a/tests/gameplay_models/test_lineup_model.py +++ b/tests/gameplay_models/test_lineup_model.py @@ -1,6 +1,7 @@ from sqlmodel import Session, select -from in_game.gameplay_models import Lineup +from in_game.gameplay_models import Game, Lineup +from in_game.gameplay_queries import get_game_lineups from factory import session_fixture @@ -18,6 +19,31 @@ def test_create_lineup(session: Session): assert lineup_id_21.position == 'C' +def test_get_game_lineups(session: Session): + this_game = session.get(Game, 1) + all_lineups = get_game_lineups(session, this_game=this_game) + away_lineups = get_game_lineups(session, this_game=this_game, specific_team=this_game.away_team) + + assert len(all_lineups) == 20 + assert len(away_lineups) == 10 + + for count in range(5): + away_lineups[count].active = False + session.add(away_lineups[count]) + + session.commit() + + all_lineups = get_game_lineups(session, this_game=this_game) + away_lineups = get_game_lineups(session, this_game=this_game, specific_team=this_game.away_team) + active_away_lineups = get_game_lineups(session, this_game=this_game, specific_team=this_game.away_team, is_active=True) + inactive_home_lineups = get_game_lineups(session, this_game=this_game, specific_team=this_game.home_team, is_active=False) + + assert len(all_lineups) == 20 + assert len(away_lineups) == 10 + assert len(active_away_lineups) == 5 + assert len(inactive_home_lineups) == 0 + + # def test_lineup_substitution(session: Session, new_games_with_lineups: list[Game]): # game_1 = new_games_with_lineups[0] # game_2 = new_games_with_lineups[1]