From f3c35390d37c3c08efe99d827d4f57095107685f Mon Sep 17 00:00:00 2001 From: MAX <63972751+ltzmax@users.noreply.github.com> Date: Mon, 12 May 2025 12:15:51 +0200 Subject: [PATCH 1/7] [honeycombs] Components v2 support. --- honeycombs/bank_utils.py | 87 ++++++++++ honeycombs/honeycombs.py | 367 ++++++++++++++++++--------------------- honeycombs/view.py | 126 +++++++++----- themoviedb/info.json | 2 +- 4 files changed, 339 insertions(+), 243 deletions(-) create mode 100644 honeycombs/bank_utils.py 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 2e9afbba..4af4cee5 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://docs.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,67 +91,95 @@ 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) - 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.") + 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']})") + 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']})" - ) - - passed_content = ( - "Passed Players:\n" + "\n".join(passed_players) - if passed_players - else "No players passed." - ) - failed_content = ( - "\n\nEliminated Players:\n" + "\n".join(failed_players) - if failed_players - else "\nNo players were eliminated." - ) - full_content = passed_content + failed_content + 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) if passed_players else "No players passed." + failed_content = "\n\nEliminated Players:\n" + "\n".join(failed_players) if failed_players else "\nNo players were eliminated." + 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", @@ -146,101 +187,68 @@ async def run_game(self, ctx): description="Check the attached file for the full results.", ) if winning_price == 0 and losing_price == 0: - embed.add_field( - name="Game Entry:", - value="This game was free to enter; no rewards were given, and no currency was lost.", - ) + embed.add_field(name="Game Entry:", value="This game was free to enter; no rewards were given, and no currency was lost.") else: - embed.add_field( - name="Winning Price:", value=f"{humanize_number(winning_price)} {currency_name}" - ) - embed.add_field( - name="Losing Price:", value=f"{humanize_number(losing_price)} {currency_name}" - ) + embed.add_field(name="Winning Price:", value=f"{humanize_number(winning_price)} {currency_name}") + embed.add_field(name="Losing Price:", value=f"{humanize_number(losing_price)} {currency_name}") 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), - ) - - 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) + 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"] + + 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." - ) - await guild_config.game_active.set(False) - await guild_config.players.clear() + 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.") + 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 +264,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 +284,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 +334,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 +343,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 +364,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 +374,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 +390,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 +402,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 +419,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 +433,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 +442,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/view.py b/honeycombs/view.py index 9a9ef6d7..61928f0b 100644 --- a/honeycombs/view.py +++ b/honeycombs/view.py @@ -30,78 +30,108 @@ log = logging.getLogger("red.maxcogs.honeycombs.view") +class JoinButton(discord.ui.Button): + def __init__(self, label: str): + super().__init__( + custom_id="join_honeycombs", + label=label, + style=discord.ButtonStyle.blurple + ) -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)" - - 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, + 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, + "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}") - 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, +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(spacing=discord.SeparatorSize.small)) + self.container.add_item( + discord.ui.TextDisplay("Click the button below to join the Sugar Honeycombs Challenge!") + ) + self.container.add_item(discord.ui.Separator(spacing=discord.SeparatorSize.small)) + self.game_details = discord.ui.TextDisplay("") # Placeholder for game details + self.container.add_item(self.game_details) + self.container.add_item(discord.ui.Separator(spacing=discord.SeparatorSize.small)) + 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(spacing=discord.SeparatorSize.small)) + 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 " ) - button.label = f"Enter The Game ({len(player_data)}/456)" - await interaction.message.edit(view=self) + + 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("An error occurred. Please try again.", ephemeral=True) diff --git a/themoviedb/info.json b/themoviedb/info.json index 2513f4ab..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, From 7c8816da46925c480bf3e3b738d4b5db9022683b Mon Sep 17 00:00:00 2001 From: MAX <63972751+ltzmax@users.noreply.github.com> Date: Mon, 12 May 2025 12:21:09 +0200 Subject: [PATCH 2/7] Style --- honeycombs/honeycombs.py | 74 +++++++++++++++++++++++++++++++--------- honeycombs/view.py | 29 ++++++++++------ 2 files changed, 76 insertions(+), 27 deletions(-) diff --git a/honeycombs/honeycombs.py b/honeycombs/honeycombs.py index 4af4cee5..038d693c 100644 --- a/honeycombs/honeycombs.py +++ b/honeycombs/honeycombs.py @@ -98,7 +98,9 @@ async def initialize_cache(self): 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() + 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): @@ -164,20 +166,36 @@ async def run_game(self, ctx: commands.Context): if random.random() < chance: 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']})") + passed_players.append( + f"Player {num}, Shape: {shape} - (User ID: {data['user_id']})" + ) else: error_messages.append(message) - passed_players.append(f"Player {num}, Shape: {shape} - (User ID: {data['user_id']}) - Deposit Failed") + passed_players.append( + f"Player {num}, Shape: {shape} - (User ID: {data['user_id']}) - Deposit Failed" + ) else: 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']})") + 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) if passed_players else "No players passed." - failed_content = "\n\nEliminated Players:\n" + "\n".join(failed_players) if failed_players else "\nNo players were eliminated." + failed_players.append( + f"Player {num}, Shape: {shape} - (User ID: {data['user_id']}) - Withdrawal Failed" + ) + + passed_content = ( + "Passed Players:\n" + "\n".join(passed_players) + if passed_players + else "No players passed." + ) + failed_content = ( + "\n\nEliminated Players:\n" + "\n".join(failed_players) + if failed_players + else "\nNo players were eliminated." + ) error_content = "\n\nErrors:\n" + "\n".join(error_messages) if error_messages else "" full_content = passed_content + failed_content + error_content @@ -187,10 +205,17 @@ async def run_game(self, ctx: commands.Context): description="Check the attached file for the full results.", ) if winning_price == 0 and losing_price == 0: - embed.add_field(name="Game Entry:", value="This game was free to enter; no rewards were given, and no currency was lost.") + embed.add_field( + name="Game Entry:", + value="This game was free to enter; no rewards were given, and no currency was lost.", + ) else: - embed.add_field(name="Winning Price:", value=f"{humanize_number(winning_price)} {currency_name}") - embed.add_field(name="Losing Price:", value=f"{humanize_number(losing_price)} {currency_name}") + embed.add_field( + name="Winning Price:", value=f"{humanize_number(winning_price)} {currency_name}" + ) + embed.add_field( + name="Losing Price:", value=f"{humanize_number(losing_price)} {currency_name}" + ) 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) @@ -215,10 +240,19 @@ async def honeycombs(self, ctx: commands.Context): 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)) + 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) @@ -227,7 +261,11 @@ async def honeycombs(self, ctx: commands.Context): 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" + 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"] @@ -243,7 +281,9 @@ async def wait_for_players(self, ctx: commands.Context, view: HoneycombView): 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 ctx.channel.send( + f"Not enough players entered the game ({len(game_state.players)}/{minimum_players}). Game has been canceled." + ) game_state.active = False game_state.players.clear() return diff --git a/honeycombs/view.py b/honeycombs/view.py index 61928f0b..8d85cc5a 100644 --- a/honeycombs/view.py +++ b/honeycombs/view.py @@ -30,12 +30,11 @@ log = logging.getLogger("red.maxcogs.honeycombs.view") + class JoinButton(discord.ui.Button): def __init__(self, label: str): super().__init__( - custom_id="join_honeycombs", - label=label, - style=discord.ButtonStyle.blurple + custom_id="join_honeycombs", label=label, style=discord.ButtonStyle.blurple ) async def callback(self, interaction: discord.Interaction): @@ -58,7 +57,7 @@ async def callback(self, interaction: discord.Interaction): 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 + ephemeral=True, ) if len(game_state.players) >= 456: @@ -75,7 +74,7 @@ async def callback(self, interaction: discord.Interaction): "user_id": interaction.user.id, "shape": shape, "passed": None, - "player_number": player_number + "player_number": player_number, } view.player_count += 1 @@ -83,12 +82,13 @@ async def callback(self, interaction: discord.Interaction): 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 + 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) @@ -99,7 +99,9 @@ def __init__(self, cog, guild): self.container = discord.ui.Container(accent_color=discord.Color.blurple()) self.container.add_item(discord.ui.Separator(spacing=discord.SeparatorSize.small)) self.container.add_item( - discord.ui.TextDisplay("Click the button below to join the Sugar Honeycombs Challenge!") + discord.ui.TextDisplay( + "Click the button below to join the Sugar Honeycombs Challenge!" + ) ) self.container.add_item(discord.ui.Separator(spacing=discord.SeparatorSize.small)) self.game_details = discord.ui.TextDisplay("") # Placeholder for game details @@ -131,7 +133,14 @@ async def on_timeout(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): + 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("An error occurred. Please try again.", ephemeral=True) + log.error( + f"Interaction error for custom_id={interaction.data.get('custom_id')}: {error}", + exc_info=True, + ) + await interaction.response.send_message( + "An error occurred. Please try again.", ephemeral=True + ) From 94502c790f7fff1b1d72d96df0cd067b05a7be66 Mon Sep 17 00:00:00 2001 From: MAX <63972751+ltzmax@users.noreply.github.com> Date: Wed, 20 Aug 2025 12:11:48 +0200 Subject: [PATCH 3/7] Apply new changes from discord.py 2.6.0 Removed spacing=discord.SeparatorSize.small: The discord.ui.Separator class in discord.py 2.6.0 does not have a spacing parameter or a SeparatorSize enum. Simply use discord.ui.Separator() to add a separator to the container. No functional impact: The Separator component is used to visually separate UI elements in the Discord message. Removing the invalid spacing parameter allows the code to run without errors while maintaining the intended layout. --- honeycombs/view.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/honeycombs/view.py b/honeycombs/view.py index 8d85cc5a..f9560b54 100644 --- a/honeycombs/view.py +++ b/honeycombs/view.py @@ -97,19 +97,19 @@ def __init__(self, cog, guild): self.player_count = 0 self.container = discord.ui.Container(accent_color=discord.Color.blurple()) - self.container.add_item(discord.ui.Separator(spacing=discord.SeparatorSize.small)) + self.container.add_item(discord.ui.Separator()) self.container.add_item( discord.ui.TextDisplay( "Click the button below to join the Sugar Honeycombs Challenge!" ) ) - self.container.add_item(discord.ui.Separator(spacing=discord.SeparatorSize.small)) - self.game_details = discord.ui.TextDisplay("") # Placeholder for game details + 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(spacing=discord.SeparatorSize.small)) + 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(spacing=discord.SeparatorSize.small)) + self.container.add_item(discord.ui.Separator()) self.add_item(self.container) async def setup(self, total_price, currency_name, minimum_players, end_time): From 1535ab9ccd1525038f9d374435f47834beaddd90 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 10:11:59 +0000 Subject: [PATCH 4/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- honeycombs/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/honeycombs/view.py b/honeycombs/view.py index f9560b54..f5db63bd 100644 --- a/honeycombs/view.py +++ b/honeycombs/view.py @@ -103,7 +103,7 @@ def __init__(self, cog, guild): "Click the button below to join the Sugar Honeycombs Challenge!" ) ) - self.container.add_item(discord.ui.Separator()) + 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()) From 9b7a2a5ba418f4ca791731b02cf78dbed70a86e7 Mon Sep 17 00:00:00 2001 From: MAX <63972751+ltzmax@users.noreply.github.com> Date: Wed, 20 Aug 2025 12:36:20 +0200 Subject: [PATCH 5/7] shorter title... literally why click... --- honeycombs/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/honeycombs/view.py b/honeycombs/view.py index f5db63bd..00713a3b 100644 --- a/honeycombs/view.py +++ b/honeycombs/view.py @@ -100,7 +100,7 @@ def __init__(self, cog, guild): self.container.add_item(discord.ui.Separator()) self.container.add_item( discord.ui.TextDisplay( - "Click the button below to join the Sugar Honeycombs Challenge!" + "Sugar Honeycombs Challenge!" ) ) self.container.add_item(discord.ui.Separator()) From f9d2df73ead431938322ef497bca2a589e44cbf2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 10:37:31 +0000 Subject: [PATCH 6/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- honeycombs/view.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/honeycombs/view.py b/honeycombs/view.py index 00713a3b..3c8bc024 100644 --- a/honeycombs/view.py +++ b/honeycombs/view.py @@ -98,11 +98,7 @@ def __init__(self, cog, guild): 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.TextDisplay("Sugar Honeycombs Challenge!")) self.container.add_item(discord.ui.Separator()) self.game_details = discord.ui.TextDisplay("") self.container.add_item(self.game_details) From 0423991e40cdd5b1112ed9ed7a3c12cb50538de7 Mon Sep 17 00:00:00 2001 From: MAX <63972751+ltzmax@users.noreply.github.com> Date: Fri, 22 Aug 2025 13:33:35 +0200 Subject: [PATCH 7/7] Update info.json --- honeycombs/info.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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",