From 82ec96bec0cc2539c08fa08f9b21ec39ab2de558 Mon Sep 17 00:00:00 2001 From: Dotsian Date: Sat, 18 Jan 2025 04:01:29 -0500 Subject: [PATCH 1/5] Allow multiple battles in one guild --- ballsdex/packages/battle/cog.py | 144 ++++++++++++++++++-------------- 1 file changed, 80 insertions(+), 64 deletions(-) diff --git a/ballsdex/packages/battle/cog.py b/ballsdex/packages/battle/cog.py index cecd569..bd71e2c 100644 --- a/ballsdex/packages/battle/cog.py +++ b/ballsdex/packages/battle/cog.py @@ -32,16 +32,22 @@ if TYPE_CHECKING: from ballsdex.core.bot import BallsDexBot + log = logging.getLogger("ballsdex.packages.battle") +battles = [] @dataclass class GuildBattle: + interaction: discord.Interaction + author: discord.Member opponent: discord.Member - author_ready: bool = False - opponent_ready: bool = False - battle: BattleInstance = field(default_factory=BattleInstance) + + author_ready = False + opponent_ready = False + + battle = field(default_factory=BattleInstance) def gen_deck(balls) -> str: @@ -103,6 +109,29 @@ def create_disabled_buttons() -> discord.ui.View: ) +def fetch_battle(interaction: discord.Interaction): + """ + Fetches a battle based on the interaction's guild ID and author/opponent's ID. + + Parameters + ---------- + interaction: discord.Interaction + The interaction you want to fetch the battle based on. + """ + found_battle = None + + for battle in battles: + if battle.interaction.guild_id != interaction.guild_id or interaction.user not in ( + battle.author, battle.opponent + ): + continue + + found_battle = battle + break + + return found_battle + + class Battle(commands.GroupCog): """ Battle your countryballs! @@ -110,23 +139,20 @@ class Battle(commands.GroupCog): def __init__(self, bot: "BallsDexBot"): self.bot = bot - self.battles: Dict[int, GuildBattle] = {} - self.interactions: Dict[int, discord.Interaction] = {} bulk = app_commands.Group( name='bulk', description='Bulk commands for battle' ) async def start_battle(self, interaction: discord.Interaction): - guild_battle = self.battles.get(interaction.guild_id) - if not guild_battle or interaction.user not in ( - guild_battle.author, - guild_battle.opponent, - ): + guild_battle = fetch_battle(interaction) + + if guild_battle is None: await interaction.response.send_message( "You aren't a part of this battle.", ephemeral=True ) return + # Set the player's readiness status if interaction.user == guild_battle.author: @@ -175,7 +201,7 @@ async def start_battle(self, interaction: discord.Interaction): discord.File(io.StringIO(battle_log), filename="battle-log.txt") ], ) - self.battles[interaction.guild_id] = None + battles.pop(battles.index(guild_battle)) else: # One player is ready, waiting for the other player @@ -213,57 +239,54 @@ async def start_battle(self, interaction: discord.Interaction): inline=True, ) - await self.interactions[interaction.guild_id].edit_original_response( - embed=embed - ) + await guild_battle.interaction.edit_original_response(embed=embed) async def cancel_battle(self, interaction: discord.Interaction): - guild_battle = self.battles.get(interaction.guild_id) + guild_battle = fetch_battle(interaction) - if interaction.user not in (guild_battle.author, guild_battle.opponent): + if guild_battle is None: await interaction.response.send_message( "You aren't a part of this battle!", ephemeral=True ) return - if guild_battle: - embed = discord.Embed( - title=f"{settings.plural_collectible_name.title()} Battle Plan", - description="The battle has been cancelled.", - color=discord.Color.red(), - ) - embed.add_field( - name=f":no_entry_sign: {guild_battle.author}'s deck:", - value=gen_deck(guild_battle.battle.p1_balls), - inline=True, - ) - embed.add_field( - name=f":no_entry_sign: {guild_battle.opponent}'s deck:", - value=gen_deck(guild_battle.battle.p2_balls), - inline=True, - ) + embed = discord.Embed( + title=f"{settings.plural_collectible_name.title()} Battle Plan", + description="The battle has been cancelled.", + color=discord.Color.red(), + ) + embed.add_field( + name=f":no_entry_sign: {guild_battle.author}'s deck:", + value=gen_deck(guild_battle.battle.p1_balls), + inline=True, + ) + embed.add_field( + name=f":no_entry_sign: {guild_battle.opponent}'s deck:", + value=gen_deck(guild_battle.battle.p2_balls), + inline=True, + ) - try: - await interaction.response.defer() - except discord.errors.InteractionResponded: - pass - await interaction.message.edit(embed=embed, view=create_disabled_buttons()) - self.battles[interaction.guild_id] = None + try: + await interaction.response.defer() + except discord.errors.InteractionResponded: + pass + + await interaction.message.edit(embed=embed, view=create_disabled_buttons()) + battles.pop(battles.index(guild_battle)) @app_commands.command() async def start(self, interaction: discord.Interaction, opponent: discord.Member): """ Start a battle with a chosen user. """ - if self.battles.get(interaction.guild_id): + if fetch_battle(interaction) is not None: await interaction.response.send_message( - "You cannot create a new battle right now, as one is already ongoing in this server.", - ephemeral=True, + "You are already in a battle.", ephemeral=True, ) return - self.battles[interaction.guild_id] = GuildBattle( - author=interaction.user, opponent=opponent - ) + + battles.append(GuildBattle(interaction, interaction.user, opponent)) + embed = update_embed([], [], interaction.user.name, opponent.name, False, False) start_button = discord.ui.Button( @@ -279,6 +302,7 @@ async def start(self, interaction: discord.Interaction, opponent: discord.Member cancel_button.callback = self.cancel_battle view = discord.ui.View(timeout=None) + view.add_item(start_button) view.add_item(cancel_button) @@ -288,11 +312,13 @@ async def start(self, interaction: discord.Interaction, opponent: discord.Member view=view, ) - self.interactions[interaction.guild_id] = interaction - async def add_balls(self, interaction: discord.Interaction, countryballs): - guild_battle = self.battles.get(interaction.guild_id) - if not guild_battle: + guild_battle = fetch_battle(interaction) + + if guild_battle is None: + await interaction.response.send_message( + "You aren't a part of a battle!", ephemeral=True + ) return # Check if the user is already ready @@ -303,13 +329,6 @@ async def add_balls(self, interaction: discord.Interaction, countryballs): f"You cannot change your {settings.plural_collectible_name} as you are already ready.", ephemeral=True ) return - # Check if user is one of the participants - - if interaction.user not in (guild_battle.author, guild_battle.opponent): - await interaction.response.send_message( - "You aren't a part of this battle!", ephemeral=True - ) - return # Determine if the user is the author or opponent and get the appropriate ball list user_balls = ( @@ -351,8 +370,12 @@ async def add_balls(self, interaction: discord.Interaction, countryballs): ) async def remove_balls(self, interaction: discord.Interaction, countryballs): - guild_battle = self.battles.get(interaction.guild_id) - if not guild_battle: + guild_battle = fetch_battle(interaction) + + if guild_battle is None: + await interaction.response.send_message( + "You aren't a part of a battle!", ephemeral=True + ) return # Check if the user is already ready @@ -363,13 +386,6 @@ async def remove_balls(self, interaction: discord.Interaction, countryballs): "You cannot change your balls as you are already ready.", ephemeral=True ) return - # Check if user is one of the participants - - if interaction.user not in (guild_battle.author, guild_battle.opponent): - await interaction.response.send_message( - "You aren't a part of this battle!", ephemeral=True - ) - return # Determine if the user is the author or opponent and get the appropriate ball list user_balls = ( From 2fee1c87c1f85454f09b8707fa346548f181cc34 Mon Sep 17 00:00:00 2001 From: Dotsian Date: Sat, 18 Jan 2025 04:08:24 -0500 Subject: [PATCH 2/5] Correct type annotation --- ballsdex/packages/battle/cog.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ballsdex/packages/battle/cog.py b/ballsdex/packages/battle/cog.py index bd71e2c..960474c 100644 --- a/ballsdex/packages/battle/cog.py +++ b/ballsdex/packages/battle/cog.py @@ -43,11 +43,11 @@ class GuildBattle: author: discord.Member opponent: discord.Member - - author_ready = False - opponent_ready = False - battle = field(default_factory=BattleInstance) + author_ready: bool = False + opponent_ready: bool = False + + battle: BattleInstance = field(default_factory=BattleInstance) def gen_deck(balls) -> str: From a4ade21c20731ce33921f1e35d3c2364517f2d1d Mon Sep 17 00:00:00 2001 From: Dotsian Date: Sat, 18 Jan 2025 04:14:39 -0500 Subject: [PATCH 3/5] Fix interaction message editing --- ballsdex/packages/battle/cog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ballsdex/packages/battle/cog.py b/ballsdex/packages/battle/cog.py index 960474c..c650b6f 100644 --- a/ballsdex/packages/battle/cog.py +++ b/ballsdex/packages/battle/cog.py @@ -358,7 +358,7 @@ async def add_balls(self, interaction: discord.Interaction, countryballs): # Update the battle embed for both players - await self.interactions[interaction.guild_id].edit_original_response( + await guild_battle.interaction.edit_original_response( embed=update_embed( guild_battle.battle.p1_balls, guild_battle.battle.p2_balls, @@ -415,7 +415,7 @@ async def remove_balls(self, interaction: discord.Interaction, countryballs): # Update the battle embed for both players - await self.interactions[interaction.guild_id].edit_original_response( + await guild_battle.interaction.edit_original_response( embed=update_embed( guild_battle.battle.p1_balls, guild_battle.battle.p2_balls, From c00c112b7efc89c3e3027037d157f9a88c9bcf82 Mon Sep 17 00:00:00 2001 From: Dotsian Date: Sat, 18 Jan 2025 04:58:44 -0500 Subject: [PATCH 4/5] Add error handling --- ballsdex/packages/battle/cog.py | 42 +++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/ballsdex/packages/battle/cog.py b/ballsdex/packages/battle/cog.py index c650b6f..62ebbbd 100644 --- a/ballsdex/packages/battle/cog.py +++ b/ballsdex/packages/battle/cog.py @@ -109,21 +109,19 @@ def create_disabled_buttons() -> discord.ui.View: ) -def fetch_battle(interaction: discord.Interaction): +def fetch_battle(user: discord.User | discord.Member): """ - Fetches a battle based on the interaction's guild ID and author/opponent's ID. + Fetches a battle based on the user provided. Parameters ---------- - interaction: discord.Interaction - The interaction you want to fetch the battle based on. + user: discord.User | discord.Member + The user you want to fetch the battle from. """ found_battle = None for battle in battles: - if battle.interaction.guild_id != interaction.guild_id or interaction.user not in ( - battle.author, battle.opponent - ): + if user not in (battle.author, battle.opponent): continue found_battle = battle @@ -145,7 +143,7 @@ def __init__(self, bot: "BallsDexBot"): ) async def start_battle(self, interaction: discord.Interaction): - guild_battle = fetch_battle(interaction) + guild_battle = fetch_battle(interaction.user) if guild_battle is None: await interaction.response.send_message( @@ -242,7 +240,7 @@ async def start_battle(self, interaction: discord.Interaction): await guild_battle.interaction.edit_original_response(embed=embed) async def cancel_battle(self, interaction: discord.Interaction): - guild_battle = fetch_battle(interaction) + guild_battle = fetch_battle(interaction.user) if guild_battle is None: await interaction.response.send_message( @@ -279,7 +277,13 @@ async def start(self, interaction: discord.Interaction, opponent: discord.Member """ Start a battle with a chosen user. """ - if fetch_battle(interaction) is not None: + if fetch_battle(opponent) is not None: + await interaction.response.send_message( + "That user is already in a battle.", ephemeral=True, + ) + return + + if fetch_battle(interaction.user) is not None: await interaction.response.send_message( "You are already in a battle.", ephemeral=True, ) @@ -313,13 +317,20 @@ async def start(self, interaction: discord.Interaction, opponent: discord.Member ) async def add_balls(self, interaction: discord.Interaction, countryballs): - guild_battle = fetch_battle(interaction) + guild_battle = fetch_battle(interaction.user) if guild_battle is None: await interaction.response.send_message( "You aren't a part of a battle!", ephemeral=True ) return + + if interaction.guild_id != guild_battle.interaction.guild_id: + await interaction.response.send_message( + "You must be in the same server as your battle to use commands.", ephemeral=True + ) + return + # Check if the user is already ready if (interaction.user == guild_battle.author and guild_battle.author_ready) or ( @@ -370,13 +381,20 @@ async def add_balls(self, interaction: discord.Interaction, countryballs): ) async def remove_balls(self, interaction: discord.Interaction, countryballs): - guild_battle = fetch_battle(interaction) + guild_battle = fetch_battle(interaction.user) if guild_battle is None: await interaction.response.send_message( "You aren't a part of a battle!", ephemeral=True ) return + + if interaction.guild_id != guild_battle.interaction.guild_id: + await interaction.response.send_message( + "You must be in the same server as your battle to use commands.", ephemeral=True + ) + return + # Check if the user is already ready if (interaction.user == guild_battle.author and guild_battle.author_ready) or ( From 4c66ac6d6dd5f4f034cdd1103ad70cf8f3a23ff6 Mon Sep 17 00:00:00 2001 From: Dotsian Date: Sat, 18 Jan 2025 05:05:27 -0500 Subject: [PATCH 5/5] Add more error handling & command descriptions --- ballsdex/packages/battle/cog.py | 61 ++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/ballsdex/packages/battle/cog.py b/ballsdex/packages/battle/cog.py index 62ebbbd..e90343e 100644 --- a/ballsdex/packages/battle/cog.py +++ b/ballsdex/packages/battle/cog.py @@ -275,8 +275,25 @@ async def cancel_battle(self, interaction: discord.Interaction): @app_commands.command() async def start(self, interaction: discord.Interaction, opponent: discord.Member): """ - Start a battle with a chosen user. + Starts a battle with a chosen user. + + Parameters + ---------- + opponent: discord.Member + The user you want to battle. """ + if opponent.user.bot: + await interaction.response.send_message( + "You can't battle against bots.", ephemeral=True, + ) + return + + if opponent.user == interaction.user: + await interaction.response.send_message( + "You can't battle against yourself.", ephemeral=True, + ) + return + if fetch_battle(opponent) is not None: await interaction.response.send_message( "That user is already in a battle.", ephemeral=True, @@ -449,9 +466,13 @@ async def add( self, interaction: discord.Interaction, countryball: BallInstanceTransform ): """ - Add a countryball to a battle. + Adds a countryball to a battle. + + Parameters + ---------- + countryball: Ball + The countryball you want to add. """ - async for dupe in self.add_balls(interaction, [countryball]): if dupe: await interaction.response.send_message( @@ -473,9 +494,13 @@ async def remove( self, interaction: discord.Interaction, countryball: BallInstanceTransform ): """ - Remove a countryball from battle. - """ + Removes a countryball from battle. + Parameters + ---------- + countryball: Ball + The countryball you want to remove. + """ async for not_in_battle in self.remove_balls(interaction, [countryball]): if not_in_battle: await interaction.response.send_message( @@ -491,12 +516,17 @@ async def remove( ephemeral=True, ) - @bulk.command(name='add') + @bulk.command(name="add") async def bulk_add( self, interaction: discord.Interaction, countryball: BallTransform ): """ - Add countryballs to a battle in bulk. + Adds countryballs to a battle in bulk. + + Parameters + ---------- + countryball: Ball + The countryball you want to add. """ player, _ = await Player.get_or_create(discord_id=interaction.user.id) balls = await countryball.ballinstances.filter(player=player) @@ -511,12 +541,12 @@ async def bulk_add( ephemeral=True, ) - @bulk.command(name='all') + @bulk.command(name="all") async def bulk_all( self, interaction: discord.Interaction ): """ - Add all your countryballs to a battle. + Adds all your countryballs to a battle. """ player, _ = await Player.get_or_create(discord_id=interaction.user.id) balls = await BallInstance.filter(player=player) @@ -530,12 +560,12 @@ async def bulk_all( await interaction.response.send_message(f"Added {count} {name}!", ephemeral=True) - @bulk.command(name='clear') + @bulk.command(name="clear") async def bulk_remove( self, interaction: discord.Interaction ): """ - Remove all your countryballs from a battle. + Removes all your countryballs from a battle. """ player, _ = await Player.get_or_create(discord_id=interaction.user.id) balls = await BallInstance.filter(player=player) @@ -549,12 +579,17 @@ async def bulk_remove( await interaction.response.send_message(f"Removed {count} {name}!", ephemeral=True) - @bulk.command(name='remove') + @bulk.command(name="remove") async def bulk_remove( self, interaction: discord.Interaction, countryball: BallTransform ): """ - Remove countryballs from a battle in bulk. + Removes countryballs from a battle in bulk. + + Parameters + ---------- + countryball: Ball + The countryball you want to remove. """ player, _ = await Player.get_or_create(discord_id=interaction.user.id) balls = await countryball.ballinstances.filter(player=player)