diff --git a/honeycombs/bank_utils.py b/honeycombs/bank_utils.py new file mode 100644 index 00000000..03b026f5 --- /dev/null +++ b/honeycombs/bank_utils.py @@ -0,0 +1,87 @@ +""" +MIT License + +Copyright (c) 2022-present ltzmax + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import discord +from redbot.core import bank +from redbot.core.errors import BankError + +# Tried to make stuff shorter in honeycombs.py but somehow made it longer, so this file got kinda pointless +# But i already had writen stuff so there's no point removing right away... i'll do someday when im less lazy. + + +async def safe_withdraw(user: discord.Member, amount: int, currency_name: str) -> tuple[bool, str]: + """ + Safely withdraw credits from a user's bank account. + + Args: + user: The Discord member to withdraw credits from. + amount: The amount to withdraw. + currency_name: The name of the currency for error messaging. + + Returns: + Tuple[bool, str]: (success, message) + - success: True if withdrawal succeeded, False otherwise. + - message: A message describing the outcome (success or error). + """ + if amount < 0: + return False, f"Cannot withdraw a negative amount of {currency_name}." + + try: + current_balance = await bank.get_balance(user) + if current_balance < amount: + return ( + False, + f"{user.mention} has insufficient {currency_name} ({current_balance} < {amount}).", + ) + + await bank.withdraw_credits(user, amount) + return True, f"Withdrew {amount} {currency_name} from {user.mention}." + + except BankError as e: + return False, f"Failed to withdraw {amount} {currency_name} from {user.mention}: {str(e)}" + + +async def safe_deposit(user: discord.Member, amount: int, currency_name: str) -> tuple[bool, str]: + """ + Safely deposit credits to a user's bank account. + + Args: + user: The Discord member to deposit credits to. + amount: The amount to deposit. + currency_name: The name of the currency for error messaging. + + Returns: + Tuple[bool, str]: (success, message) + - success: True if deposit succeeded, False otherwise. + - message: A message describing the outcome (success or error). + """ + if amount < 0: + return False, f"Cannot deposit a negative amount of {currency_name}." + + try: + await bank.deposit_credits(user, amount) + return True, f"Deposited {amount} {currency_name} to {user.mention}." + + except BankError as e: + return False, f"Failed to deposit {amount} {currency_name} to {user.mention}: {str(e)}" diff --git a/honeycombs/honeycombs.py b/honeycombs/honeycombs.py index a65285f3..0746fada 100644 --- a/honeycombs/honeycombs.py +++ b/honeycombs/honeycombs.py @@ -36,38 +36,51 @@ from redbot.core.utils.chat_formatting import header, humanize_list, humanize_number, hyperlink from redbot.core.utils.views import ConfirmView, SimpleMenu +from .bank_utils import safe_deposit, safe_withdraw from .view import HoneycombView log = logging.getLogger("red.maxcogs.honeycombs") +class GameState: + def __init__(self, guild: discord.Guild): + self.guild = guild + self.active = False + self.players = {} + self.start_time = None + self.end_time = None + + class HoneyCombs(commands.Cog): """Play a game similar to Sugar Honeycombs, inspired by the Netflix series Squid Game.""" - __version__: Final[str] = "1.4.0" + __version__: Final[str] = "2.0.0a" __author__: Final[str] = "MAX" __docs__: Final[str] = "https://cogs.maxapp.tv/" def __init__(self, bot: Red): self.bot = bot self.config = Config.get_conf(self, identifier=34562809777, force_registration=True) + self.cache = {"global": {}, "guilds": {}} + self.game_states = {} + self.locks = {} + self.session = aiohttp.ClientSession() default_guild = { - "players": {}, # List of players - "game_active": False, # Default game is inactive - "default_start_image": "https://i.maxapp.tv/4c76241E.png", # Default start image - "shapes": ["circle⭕️", "triangle△", "star⭐️", "umbrella☂️"], # Default shapes - "mod_only_command": False, # Default mod only command is False - "minimum_players": 5, # Default minimum number of players - "default_minutes": 10, # Default minutes to 10. - "default_start_minutes": 2, # Default start minutes to 2 + "players": {}, + "game_active": False, + "default_start_image": "https://i.maxapp.tv/4c76241E.png", + "shapes": ["circle⭕️", "triangle△", "star⭐️", "umbrella☂️"], + "mod_only_command": False, + "minimum_players": 2, + "default_minutes": 10, } default_global = { - "winning_price": 100, # Default winning price - "losing_price": 100, # Default losing price + "winning_price": 100, + "losing_price": 100, } self.config.register_global(**default_global) self.config.register_guild(**default_guild) - self.session = aiohttp.ClientSession() + self.bot.loop.create_task(self.initialize_cache()) def format_help_for_context(self, ctx: commands.Context) -> str: """Thanks Sinbad!""" @@ -78,55 +91,100 @@ async def red_delete_data_for_user(self, **kwargs: Any) -> None: """Nothing to delete.""" return - async def cog_unload(self) -> None: + async def initialize_cache(self): + self.cache["global"] = await self.config.all() + for guild in self.bot.guilds: + self.cache["guilds"][guild.id] = await self.config.guild(guild).all() + + def get_guild_config(self, guild: discord.Guild): + if guild.id not in self.cache["guilds"]: + self.cache["guilds"][guild.id] = self.bot.loop.create_task( + self.config.guild(guild).all() + ).result() + return self.cache["guilds"][guild.id] + + async def update_guild_config(self, guild: discord.Guild, key: str, value: Any): + self.cache["guilds"][guild.id][key] = value + await self.config.guild(guild).set_raw(key, value=value) + + async def update_global_config(self, key: str, value: Any): + self.cache["global"][key] = value + await self.config.set_raw(key, value=value) + + def get_game_state(self, guild: discord.Guild) -> GameState: + if guild.id not in self.game_states: + self.game_states[guild.id] = GameState(guild) + return self.game_states[guild.id] + + def get_lock(self, guild: discord.Guild) -> asyncio.Lock: + if guild.id not in self.locks: + self.locks[guild.id] = asyncio.Lock() + return self.locks[guild.id] + + async def cog_unload(self): await self.session.close() for guild in self.bot.guilds: await self.config.guild(guild).players.clear() await self.config.guild(guild).game_active.set(False) - async def run_game(self, ctx): - guild_config = self.config.guild(ctx.guild) - players = await guild_config.players() - if not players: + async def run_game(self, ctx: commands.Context): + game_state = self.get_game_state(ctx.guild) + guild_config = self.get_guild_config(ctx.guild) + + if not game_state.players: await ctx.send("No players joined the game.") - await guild_config.game_active.set(False) + game_state.active = False return - default_minutes = await guild_config.default_minutes() - end_time = int((datetime.now() + timedelta(minutes=default_minutes)).timestamp()) + default_minutes = guild_config["default_minutes"] + game_state.end_time = datetime.now() + timedelta(minutes=default_minutes) + end_timestamp = int(game_state.end_time.timestamp()) + embed = discord.Embed( title="Sugar Honeycombs Challenge", color=await ctx.embed_color(), - description=f"Let the game begin!\nThe bot will finish to decide whether you Pass or Eliminated.\nGood luck!", + description=f"Let the game begin!\nThe bot will finish to decide whether you Pass or Eliminated.\nGood luck!", ) - embed.add_field(name="Players:", value=len(players)) + embed.add_field(name="Players:", value=len(game_state.players)) embed.set_footer(text="I would like to expend a heartfelt welcome to you all.") await ctx.send(embed=embed) - await discord.utils.sleep_until(datetime.now() + timedelta(minutes=default_minutes)) - winning_price = await self.config.winning_price() - losing_price = await self.config.losing_price() + await asyncio.sleep(default_minutes * 60) + + winning_price = self.cache["global"]["winning_price"] + losing_price = self.cache["global"]["losing_price"] currency_name = await bank.get_currency_name(ctx.guild) passed_players, failed_players = [], [] - for num, data in players.items(): + error_messages = [] + + for num, data in game_state.players.items(): user = ctx.guild.get_member(data["user_id"]) if user: shape = data["shape"] chance = 0.08 if shape == "umbrella☂️" else 0.2 if random.random() < chance: - try: - await bank.deposit_credits(user, winning_price) + success, message = await safe_deposit(user, winning_price, currency_name) + if success: passed_players.append( f"Player {num}, Shape: {shape} - (User ID: {data['user_id']})" ) - except errors.BalanceTooHigh: - log.error("User's balance is too high for the winning price of the game.") + else: + error_messages.append(message) + passed_players.append( + f"Player {num}, Shape: {shape} - (User ID: {data['user_id']}) - Deposit Failed" + ) else: - await bank.withdraw_credits(user, losing_price) - failed_players.append( - f"Player {num}, Shape: {shape} - (User ID: {data['user_id']})" - ) + success, message = await safe_withdraw(user, losing_price, currency_name) + if success: + failed_players.append( + f"Player {num}, Shape: {shape} - (User ID: {data['user_id']})" + ) + else: + error_messages.append(message) + failed_players.append( + f"Player {num}, Shape: {shape} - (User ID: {data['user_id']}) - Withdrawal Failed" + ) passed_content = ( "Passed Players:\n" + "\n".join(passed_players) @@ -138,7 +196,8 @@ async def run_game(self, ctx): if failed_players else "\nNo players were eliminated." ) - full_content = passed_content + failed_content + error_content = "\n\nErrors:\n" + "\n".join(error_messages) if error_messages else "" + full_content = passed_content + failed_content + error_content embed = discord.Embed( title="Here is your Results", @@ -160,87 +219,76 @@ async def run_game(self, ctx): embed.set_footer(text="Thank you for playing!") file = discord.File(StringIO(full_content), filename="honeycombs_results.txt") await ctx.send(embed=embed, file=file) - await guild_config.players.clear() - await guild_config.game_active.set(False) + + game_state.players.clear() + game_state.active = False + await self.update_guild_config(ctx.guild, "players", {}) + await self.update_guild_config(ctx.guild, "game_active", False) @commands.guild_only() - @commands.hybrid_command(name="honeycombs") + @commands.hybrid_command() @commands.cooldown(1, 60, commands.BucketType.guild) @commands.bot_has_permissions(embed_links=True, attach_files=True) async def honeycombs(self, ctx: commands.Context): """ Start a game of Sugar Honeycombs. - You need at least 5 players to start the game. + You need at least 2 players to start the game. The maximum number of players is 456. """ - guild_config = self.config.guild(ctx.guild) - if await guild_config.game_active(): - return await ctx.send( - "A game is already in progress in this server.", - reference=ctx.message.to_reference(fail_if_not_exists=False), + guild_config = self.get_guild_config(ctx.guild) + game_state = self.get_game_state(ctx.guild) + async with self.get_lock(ctx.guild): + if game_state.active: + return await ctx.send( + "A game is already in progress in this server.", + reference=ctx.message.to_reference(fail_if_not_exists=False), + ) + + if ( + guild_config["mod_only_command"] + and not ctx.author.guild_permissions.manage_messages + ): + return await ctx.send( + "This command is only available to moderators.", + reference=ctx.message.to_reference(fail_if_not_exists=False), + ) + + game_state.active = True + game_state.start_time = datetime.now() + timedelta(minutes=2) + end_time = int(game_state.start_time.timestamp()) + view = HoneycombView(self, ctx.guild) + + winning_price = self.cache["global"]["winning_price"] + losing_price = self.cache["global"]["losing_price"] + total_price = ( + humanize_number(winning_price + losing_price) + if (winning_price + losing_price) != 0 + else "Free" ) + currency_name = await bank.get_currency_name(ctx.guild) + minimum_players = guild_config["minimum_players"] - if ( - await guild_config.mod_only_command() - and not ctx.author.guild_permissions.manage_messages - ): - return await ctx.send( - "This command is only available to moderators.", - reference=ctx.message.to_reference(fail_if_not_exists=False), - ) - - await guild_config.game_active.set(True) - default_start_time = await guild_config.default_start_minutes() - end_time = int((datetime.now() + timedelta(minutes=default_start_time)).timestamp()) - view = HoneycombView(self, ctx.guild) - winning_price = await self.config.winning_price() - losing_price = await self.config.losing_price() - total_price = winning_price + losing_price - currency_name = await bank.get_currency_name(ctx.guild) - - embed = discord.Embed( - title="Sugar Honeycombs Challenge", - color=await ctx.embed_color(), - description=f"Click the button to join! Game starts .", - ) - embed.add_field( - name="Price To Enter:", - value=( - f"{humanize_number(total_price)} {currency_name}" - if total_price != 0 - else "Free To Enter" - ), - ) - - img = await guild_config.default_start_image() - if img: - embed.set_image(url=img) - minimum_players = await guild_config.minimum_players() - embed.set_footer(text=f"Need at least {minimum_players} players to start the game.") - - message = await ctx.send(embed=embed, view=view) - view.message = message - await self.wait_for_players(ctx, view) + await view.setup(total_price, currency_name, minimum_players, end_time) + message = await ctx.send(view=view) + view.message = message + await self.wait_for_players(ctx, view) async def wait_for_players(self, ctx: commands.Context, view: HoneycombView): - guild_config = self.config.guild(ctx.guild) - default_start_minutes = await guild_config.default_start_minutes() - minimum_players = await guild_config.minimum_players() - await discord.utils.sleep_until(datetime.now() + timedelta(minutes=default_start_minutes)) - - players = await guild_config.players() - if len(players) < minimum_players: - await ctx.send( - f"Not enough players entered the game ({len(players)}/{minimum_players}). Game has been canceled." + guild_config = self.get_guild_config(ctx.guild) + game_state = self.get_game_state(ctx.guild) + minimum_players = guild_config["minimum_players"] + await asyncio.sleep(120) + + if len(game_state.players) < minimum_players: + await ctx.channel.send( + f"Not enough players entered the game ({len(game_state.players)}/{minimum_players}). Game has been canceled." ) - await guild_config.game_active.set(False) - await guild_config.players.clear() + game_state.active = False + game_state.players.clear() return - for item in view.children: - item.disabled = True - await view.message.edit(view=view) + await view.on_timeout() await self.run_game(ctx) @commands.group(aliases=["squidgame", "sg"]) @@ -256,15 +304,12 @@ async def checklist(self, ctx: commands.Context): This command will show the list of players who have joined the game along with their player numbers. """ - players = await self.config.guild(ctx.guild).players() - if not players: + game_state = self.get_game_state(ctx.guild) + if not game_state.players: return await ctx.send("No ongoing game found.") - player_list = [] - for player_number, player_data in players.items(): - title = "List of players" - player_list.append(f"{header(title, 'medium')}\nPlayer {player_number}") - pages = [humanize_list(player_list[i : i + 10]) for i in range(0, len(player_list), 10)] + player_list = [f"Player {player_number}" for player_number in game_state.players.keys()] + pages = [humanize_number(player_list[i : i + 10]) for i in range(0, len(player_list), 10)] await SimpleMenu( pages, disable_after_timeout=True, @@ -279,14 +324,27 @@ async def reset(self, ctx: commands.Context): Allow administrators to reset the game settings for the server in case of any issues or to start fresh again. """ - data = await self.config.guild(ctx.guild).all() - if not data["players"] and not data["game_active"]: + game_state = self.get_game_state(ctx.guild) + guild_config = self.get_guild_config(ctx.guild) + if not game_state.players and not game_state.active: return await ctx.send("Game settings are already reset.") view = ConfirmView(ctx.author, disable_buttons=True) message = await ctx.send("Are you sure you want to reset the game settings?", view=view) await view.wait() if view.result: + game_state.players.clear() + game_state.active = False + default_guild = { + "players": {}, + "game_active": False, + "default_start_image": "https://i.maxapp.tv/4c76241E.png", + "shapes": ["circle⭕️", "triangle△", "star⭐️", "umbrella☂️"], + "mod_only_command": False, + "minimum_players": 5, + "default_minutes": 10, + } + self.cache["guilds"][ctx.guild.id] = default_guild await self.config.guild(ctx.guild).clear() await ctx.send("Game settings have been reset.") else: @@ -316,10 +374,8 @@ async def setimage(self, ctx: commands.Context, *, image_url: Optional[str] = No data = await r.read() except aiohttp.ClientError as e: return await ctx.send("Failed to set the start image. Client error.") - log.error(e) except asyncio.TimeoutError as e: return await ctx.send("Failed to set the start image. Timeout error.") - log.error(e) image_formats = ["JPG", "JPEG", "PNG", "GIF", "WEBP"] if not data or not any(data.startswith(bytes(f"{sig}", "utf-8")) for sig in image_formats): @@ -327,14 +383,15 @@ async def setimage(self, ctx: commands.Context, *, image_url: Optional[str] = No f"Failed to set the start image. Only {humanize_list(image_formats)} format is supported." ) - await self.config.guild(ctx.guild).default_start_image.set(image_url) + await self.update_guild_config(ctx.guild, "default_start_image", image_url) await ctx.send("The start image has been set.") @commands.admin() @setimage.command(name="clear", aliases=["reset"]) async def setimage_clear(self, ctx: commands.Context): """Reset the start image to default.""" - await self.config.guild(ctx.guild).default_start_image.clear() + default_image = "https://i.maxapp.tv/4c76241E.png" + await self.update_guild_config(ctx.guild, "default_start_image", default_image) await ctx.send("Reset the start image.") @commands.admin() @@ -347,8 +404,8 @@ async def mod_only(self, ctx: commands.Context, state: Optional[bool] = None): If set to False, anyone can start the game. """ if state is None: - state = not await self.config.guild(ctx.guild).mod_only_command() - await self.config.guild(ctx.guild).mod_only_command.set(state) + state = not self.get_guild_config(ctx.guild)["mod_only_command"] + await self.update_guild_config(ctx.guild, "mod_only_command", state) await ctx.send(f"Mod only command has been set to {state}.") @commands.admin() @@ -357,40 +414,12 @@ async def endtime(self, ctx: commands.Context, default_minutes: commands.Range[i """ Change the default minutes for when the game should end. - **Please note** - - This is for the length of the game to run for, not the length of when the game starts. - The default minutes is 10. The maximum number of minutes is 720 (12 hours). - - **Examples**: - - `[p]honeycombset defaultminutes 20` - Set the default number of minutes to 20 minutes. - - `[p]honeycombset defaultminutes 60` - Set the default number of minutes to 60 minutes. (1 hour) - You can use [unitconverters](https://www.unitconverters.net/time/minutes-to-hours.htm) to convert minutes to hours. - - **Arguments**: - - ``: The default number of minutes for the game. """ - await self.config.guild(ctx.guild).default_minutes.set(default_minutes) + await self.update_guild_config(ctx.guild, "default_minutes", default_minutes) await ctx.send(f"The default minutes has been set to {default_minutes} minutes.") - @commands.admin() - @honeycombset.command(name="starttime") - async def starttime( - self, ctx: commands.Context, default_start_minutes: commands.Range[int, 2, 20] - ): - """ - Change the default minutes for the game to start for. - - **Please note** - - This is for the length of the game to start for, not the length of when the game ends. - - The default minutes is 2. - The maximum minutes is 20. - """ - await self.config.guild(ctx.guild).default_start_minutes.set(default_start_minutes) - await ctx.send(f"The default minutes has been set to {default_start_minutes} minutes.") - @commands.admin() @honeycombset.command(name="minimumplayers") async def minimum_players( @@ -401,7 +430,7 @@ async def minimum_players( The default minimum number of players is 5. """ - await self.config.guild(ctx.guild).minimum_players.set(minimum_players) + await self.update_guild_config(ctx.guild, "minimum_players", minimum_players) await ctx.send(f"The minimum number of players has been set to {minimum_players}.") @commands.is_owner() @@ -413,12 +442,10 @@ async def winning_price( Set the winning price for the game. Set the price to 0 to disable the winning price. - The default winning price is 100 credits. - The winning price is the amount of credits a player will receive if they pass the game. """ currency_name = await bank.get_currency_name(ctx.guild) - await self.config.winning_price.set(amount) + await self.update_global_config("winning_price", amount) await ctx.send( f"The winning price has been set to {humanize_number(amount)} {currency_name}." ) @@ -432,12 +459,10 @@ async def losing_price( Set the losing price for the game. Set the price to 0 to disable the losing price. - The default losing price is 100 credits. - The losing price is the amount of credits a player will lose if they fail the game. """ currency_name = await bank.get_currency_name(ctx.guild) - await self.config.losing_price.set(amount) + await self.update_global_config("losing_price", amount) await ctx.send( f"The losing price has been set to {humanize_number(amount)} {currency_name}." ) @@ -448,8 +473,8 @@ async def losing_price( @commands.bot_has_permissions(embed_links=True) async def settings(self, ctx: commands.Context): """View the current game settings.""" - guild_data = await self.config.guild(ctx.guild).all() - global_data = await self.config.all() + guild_config = self.get_guild_config(ctx.guild) + global_config = self.cache["global"] currency_name = await bank.get_currency_name(ctx.guild) embed = discord.Embed( @@ -457,38 +482,32 @@ async def settings(self, ctx: commands.Context): description="View the current game settings.", color=await ctx.embed_color(), ) - # Only owner can see the winning and losing price - # since they are the only ones who can change them. if await self.bot.is_owner(ctx.author): embed.add_field( name="Winning Price", - value=f"{humanize_number(global_data.get('winning_price', 0))} {currency_name}", + value=f"{humanize_number(global_config.get('winning_price', 0))} {currency_name}", ) embed.add_field( name="Losing Price", - value=f"{humanize_number(global_data.get('losing_price', 0))} {currency_name}", + value=f"{humanize_number(global_config.get('losing_price', 0))} {currency_name}", ) embed.add_field( - name="Mod Only Command", value=guild_data.get("mod_only_command", False), inline=False + name="Mod Only Command", + value=guild_config.get("mod_only_command", False), + inline=False, ) embed.add_field( - name="Minimum Players", value=guild_data.get("minimum_players", 5), inline=False + name="Minimum Players", value=guild_config.get("minimum_players", 5), inline=False ) embed.add_field( name="Default ongoing game minutes", - value=guild_data.get("default_minutes", 10), + value=guild_config.get("default_minutes", 10), inline=False, ) - embed.add_field( - name="Default Start Minutes", - value=guild_data.get("default_start_minutes", 2), - inline=False, - ) - start_image_url = guild_data.get("default_start_image", None) + start_image_url = guild_config.get("default_start_image", None) if start_image_url: - start_image_value = hyperlink("View Start Image", start_image_url) + start_image_value = f"[View Start Image]({start_image_url})" else: start_image_value = "Not set" embed.add_field(name="Start Image", value=start_image_value, inline=False) - await ctx.send(embed=embed) diff --git a/honeycombs/info.json b/honeycombs/info.json index ba151216..808901ac 100644 --- a/honeycombs/info.json +++ b/honeycombs/info.json @@ -7,7 +7,7 @@ "description": "Play a game similar to Sugar Honeycombs, inspired by the Netflix series Squid Game.", "short": "Play a game similar to Sugar Honeycombs, inspired by the Netflix series Squid Game.", "hidden": false, - "min_bot_version": "3.5.14", + "min_bot_version": "3.5.21", "tags": [ "squid game", "honeycombs", diff --git a/honeycombs/view.py b/honeycombs/view.py index 9a9ef6d7..3c8bc024 100644 --- a/honeycombs/view.py +++ b/honeycombs/view.py @@ -31,77 +31,112 @@ log = logging.getLogger("red.maxcogs.honeycombs.view") -class HoneycombView(discord.ui.View): - def __init__(self, game, guild): - super().__init__(timeout=120) - self.game = game - self.guild = guild - self.join_button.label = "Enter The Game (Loading...)" - self.player_count = 0 - - async def setup(self): - player_data = await self.game.config.guild(self.guild).players() - self.player_count = len(player_data) - self.join_button.label = f"Enter The Game ({self.player_count}/456)" +class JoinButton(discord.ui.Button): + def __init__(self, label: str): + super().__init__( + custom_id="join_honeycombs", label=label, style=discord.ButtonStyle.blurple + ) - async def on_timeout(self): - for item in self.children: - item: discord.ui.Item - item.disabled = True - try: - await self.message.edit(view=self) - except discord.HTTPException as e: - log.error(e) - - @discord.ui.button( - custom_id="join_honeycombs", - label="Enter The Game (0/456)", - style=discord.ButtonStyle.blurple, - ) - async def join_button(self, interaction: discord.Interaction, button: discord.ui.Button): - credit_name = await bank.get_currency_name(interaction.guild) - winning_price = await self.game.config.winning_price() - losing_price = await self.game.config.losing_price() + async def callback(self, interaction: discord.Interaction): + view: HoneycombView = self.view + game_state = view.cog.get_game_state(view.guild) + guild_config = view.cog.get_guild_config(view.guild) + currency_name = await bank.get_currency_name(interaction.guild) + winning_price = view.cog.cache["global"]["winning_price"] + losing_price = view.cog.cache["global"]["losing_price"] user_balance = await bank.get_balance(interaction.user) if user_balance < winning_price + losing_price: return await interaction.response.send_message( - f"You do not have enough {credit_name} to enter the game.", ephemeral=True + f"You do not have enough {currency_name} to enter the game.", ephemeral=True ) - player_data = await self.game.config.guild(interaction.guild).players() - player_ids = {player["user_id"] for player in player_data.values()} - + player_ids = {data["user_id"] for data in game_state.players.values()} if interaction.user.id in player_ids: - for number, data in player_data.items(): + for number, data in game_state.players.items(): if data["user_id"] == interaction.user.id: return await interaction.response.send_message( f"You are already in the game as `Player {number}` with shape {data['shape']}. You cannot leave.", ephemeral=True, ) - if len(player_data) >= 456: + if len(game_state.players) >= 456: return await interaction.response.send_message( "The game is already full. Please wait for the next game to start.", ephemeral=True ) - available_numbers = [i for i in range(1, 457) if str(i) not in player_data] + available_numbers = [i for i in range(1, 457) if i not in game_state.players] player_number = random.choice(available_numbers) - shapes = await self.game.config.guild(interaction.guild).shapes() + shapes = guild_config["shapes"] shape = random.choice(shapes) - player_data[str(player_number)] = { + game_state.players[player_number] = { "user_id": interaction.user.id, "shape": shape, "passed": None, "player_number": player_number, } - await self.game.config.guild(interaction.guild).players.set(player_data) + view.player_count += 1 + self.label = f"Enter The Game ({view.player_count}/456)" + try: + await interaction.response.send_message( + f"You have joined the game as `Player {player_number}`.\nYour shape is {shape}\nThis message will disappear, but your number and shape are recorded!", + ephemeral=True, + ) + await interaction.message.edit(view=view) + except discord.HTTPException as e: + log.error(f"Failed to edit message: {e}") + +class HoneycombView(discord.ui.LayoutView): + def __init__(self, cog, guild): + super().__init__(timeout=120) + self.cog = cog + self.guild = guild + self.player_count = 0 + + self.container = discord.ui.Container(accent_color=discord.Color.blurple()) + self.container.add_item(discord.ui.Separator()) + self.container.add_item(discord.ui.TextDisplay("Sugar Honeycombs Challenge!")) + self.container.add_item(discord.ui.Separator()) + self.game_details = discord.ui.TextDisplay("") + self.container.add_item(self.game_details) + self.container.add_item(discord.ui.Separator()) + self.join_button = JoinButton(label="Enter The Game (0/456)") + self.container.add_item(discord.ui.ActionRow(self.join_button)) + self.container.add_item(discord.ui.Separator()) + self.add_item(self.container) + + async def setup(self, total_price, currency_name, minimum_players, end_time): + """Initialize the player count and game details.""" + game_state = self.cog.get_game_state(self.guild) + self.player_count = len(game_state.players) + self.join_button.label = f"Enter The Game ({self.player_count}/456)" + self.game_details.content = ( + f"Price to Enter: {total_price} {currency_name}\n" + f"Minimum Players: {minimum_players}\n" + f"Game starts " + ) + + async def on_timeout(self): + """Disable the button on timeout and update the message.""" + for child in self.walk_children(): + if isinstance(child, discord.ui.Button): + child.disabled = True + try: + await self.message.edit(view=self) + except discord.HTTPException as e: + log.error(f"Failed to edit message on timeout: {e}") + + async def on_error( + self, interaction: discord.Interaction, error: Exception, item: discord.ui.Item + ): + """Handle interaction errors.""" + log.error( + f"Interaction error for custom_id={interaction.data.get('custom_id')}: {error}", + exc_info=True, + ) await interaction.response.send_message( - f"You have joined the game as `Player {player_number}`.\nYour shape is {shape}\nThis message will disappear, but your number and shape are recorded!", - ephemeral=True, + "An error occurred. Please try again.", ephemeral=True ) - button.label = f"Enter The Game ({len(player_data)}/456)" - await interaction.message.edit(view=self) diff --git a/themoviedb/info.json b/themoviedb/info.json index 425d8743..f40aea6d 100644 --- a/themoviedb/info.json +++ b/themoviedb/info.json @@ -3,7 +3,7 @@ "max" ], "name": "TheMovieDB", - "install_msg": "Thanks for installing.\nYou will need to set your API key before using this cog. See `[p]tmdbset creds`.\n for documentation.\nIf you enjoy my work, you can donate at [buymeacoffee]()", + "install_msg": "Thanks for installing.\nYou will need to set your API key before using this cog. See `[p]tmdbset creds`.\n for documentation.\nIf you enjoy my work, you can donate at [buymeacoffee]()\n## PLEASE NOTE:\nThis cog is in alpha phase and using Components V2 and is not fully tested or and stable enough for production use. This also require the Components V2 PR from discord.py for this cog to work.", "description": "Search for informations of movies and TV shows from themoviedb.org.", "short": "Search for informations of movies and TV shows from themoviedb.org.", "hidden": false,