From 1f6fe40edb98d83a16edccfaf9aa65dd785f5dd3 Mon Sep 17 00:00:00 2001 From: TheKrol Date: Wed, 25 Dec 2024 09:14:28 -0500 Subject: [PATCH 01/17] Updating hangman to fix all the issues --- techsupport_bot/commands/hangman.py | 208 ++++++++++++++++++++-------- 1 file changed, 147 insertions(+), 61 deletions(-) diff --git a/techsupport_bot/commands/hangman.py b/techsupport_bot/commands/hangman.py index 12300e9c..b2ae40bc 100644 --- a/techsupport_bot/commands/hangman.py +++ b/techsupport_bot/commands/hangman.py @@ -4,11 +4,12 @@ import datetime import uuid -from typing import TYPE_CHECKING, Self +from typing import TYPE_CHECKING, Self, Union import discord import ui from core import auxiliary, cogs, extensionconfig +from discord import app_commands from discord.ext import commands if TYPE_CHECKING: @@ -39,7 +40,6 @@ class HangmanGame: Attributes: HANG_PICS (list[str]): The list of hangman pictures - FINAL_STEP (int): The last step of the hangman game finished (bool): Determines if the game has been finished or not failed (bool): Determines if the players failed to guess the word @@ -108,14 +108,14 @@ class HangmanGame: | =========""", ] - FINAL_STEP: int = len(HANG_PICS) - 1 - def __init__(self: Self, word: str) -> None: + def __init__(self: Self, word: str, max_guesses: int = 6) -> None: if not word or "_" in word or not word.isalpha(): raise ValueError("valid word must be provided") self.word = word self.guesses = set() self.step = 0 + self.max_guesses = max_guesses self.started = datetime.datetime.utcnow() self.id = uuid.uuid4() @@ -139,7 +139,11 @@ def draw_hang_state(self: Self) -> str: Returns: str: The str representation of the correct picture """ - return self.HANG_PICS[self.step] + picture_index = min( + len(self.HANG_PICS) - 1, # Maximum valid index + int(self.step / self.max_guesses * (len(self.HANG_PICS) - 1)), + ) + return self.HANG_PICS[picture_index] def guess(self: Self, letter: str) -> bool: """Registers a guess to the given game @@ -169,7 +173,7 @@ def guess(self: Self, letter: str) -> bool: @property def finished(self: Self) -> bool: """Method to finish the game of hangman.""" - if self.step < 0 or self.step >= self.FINAL_STEP: + if self.step < 0 or self.step >= self.max_guesses: return True if all(letter in self.guesses for letter in self.word): return True @@ -178,7 +182,7 @@ def finished(self: Self) -> bool: @property def failed(self: Self) -> bool: """Method in case the game wasn't successful.""" - if self.step >= self.FINAL_STEP: + if self.step >= self.max_guesses: return True return False @@ -201,6 +205,14 @@ def guessed(self: Self, letter: str) -> bool: return True return False + def remaining_guesses(self) -> int: + """Returns the number of guesses remaining.""" + return self.max_guesses - self.step + + def add_guesses(self, num_guesses: int) -> None: + """Adds more guesses to the game.""" + self.max_guesses += num_guesses + async def can_stop_game(ctx: commands.Context) -> bool: """Checks if a user has the ability to stop the running game @@ -263,64 +275,74 @@ async def hangman(self: Self, ctx: commands.Context) -> None: # Executed if there are no/invalid args supplied await auxiliary.extension_help(self, ctx, self.__module__[9:]) - @hangman.command( - name="start", - description="Starts a hangman game in the current channel", - usage="[word]", + @app_commands.command( + name="start_hangman", + description="Start a Hangman game in the current channel.", + extras={"module": "hangman"}, ) - async def start_game(self: Self, ctx: commands.Context, word: str) -> None: - """Method to start the hangman game and delete the original message. - This is a command and should be access via discord + async def start_game( + self: Self, interaction: discord.Interaction, word: str + ) -> None: + """Slash command to start a hangman game. Args: - ctx (commands.Context): The context in which the command occured - word (str): The word to state the hangman game with + interaction (discord.Interaction): The interaction object from Discord. + word (str): The word to start the Hangman game with. """ - # delete the message so the word is not seen - await ctx.message.delete() + # Ensure only the command's author can see this interaction + await interaction.response.defer(ephemeral=True) - game_data = self.games.get(ctx.channel.id) + # Check if a game is already active in the channel + game_data = self.games.get(interaction.channel_id) if game_data: - # if there is a game currently, - # get user who started it + # Check if the game owner wants to overwrite the current game user = game_data.get("user") - if getattr(user, "id", 0) == ctx.author.id: + if user.id == interaction.user.id: view = ui.Confirm() await view.send( - message=( - "There is a current game in progress. Would you like to end it?" - ), - channel=ctx.channel, - author=ctx.author, + message="There is a current game in progress. Do you want to end it?", + channel=interaction.channel, + author=interaction.user, ) - await view.wait() - if view.value is ui.ConfirmResponse.TIMEOUT: - return - if view.value is ui.ConfirmResponse.DENIED: - await auxiliary.send_deny_embed( - message="The current game was not ended", channel=ctx.channel + if view.value in [ + ui.ConfirmResponse.TIMEOUT, + ui.ConfirmResponse.DENIED, + ]: + await interaction.followup.send( + "The current game was not ended.", ephemeral=True ) return - del self.games[ctx.channel.id] + # Remove the existing game + del self.games[interaction.channel_id] else: - await auxiliary.send_deny_embed( - message="There is a game in progress for this channel", - channel=ctx.channel, + await interaction.followup.send( + "A game is already in progress for this channel.", ephemeral=True ) return - game = HangmanGame(word=word) - embed = await self.generate_game_embed(ctx, game) - message = await ctx.channel.send(embed=embed) - self.games[ctx.channel.id] = { - "user": ctx.author, + # Validate the provided word + try: + game = HangmanGame(word=word.lower()) + except ValueError as e: + await interaction.followup.send(f"Invalid word: {e}", ephemeral=True) + return + + # Create and send the initial game embed + embed = await self.generate_game_embed(interaction, game) + message = await interaction.channel.send(embed=embed) + self.games[interaction.channel_id] = { + "user": interaction.user, "game": game, "message": message, "last_guesser": None, } + await interaction.followup.send( + f"The Hangman game has started with a hidden word!", ephemeral=True + ) + @hangman.command( name="guess", description="Guesses a letter for the current hangman game", @@ -333,6 +355,14 @@ async def guess(self: Self, ctx: commands.Context, letter: str) -> None: ctx (commands.Context): The context in which the command was run in letter (str): The letter the user is trying to guess """ + game_data = self.games.get(ctx.channel.id) + if ctx.author == game_data.get("user"): + await auxiliary.send_deny_embed( + message="You cannot guess letters because you started this game!", + channel=ctx.channel, + ) + return + if len(letter) > 1 or not letter.isalpha(): await auxiliary.send_deny_embed( message="You can only guess a letter", channel=ctx.channel @@ -367,31 +397,19 @@ async def guess(self: Self, ctx: commands.Context, letter: str) -> None: await ctx.send(content=content) async def generate_game_embed( - self: Self, ctx: commands.Context, game: HangmanGame + self: Self, ctx_or_interaction: Union[discord.Interaction, commands.Context], game: HangmanGame ) -> discord.Embed: - """Takes a game state and makes it into a pretty embed - Does not send the embed - - Args: - ctx (commands.Context): The context in which the game command needing - a drawing was called in - game (HangmanGame): The hangman game to draw into an embed - - Returns: - discord.Embed: The ready and styled embed containing the current state of the game - """ + """Generate an embed for the current game state.""" hangman_drawing = game.draw_hang_state() hangman_word = game.draw_word_state() - prefix = await self.bot.get_prefix(ctx.message) embed = discord.Embed( title=f"`{hangman_word}`", - description=( - f"Type `{prefix}help extension hangman` for more info\n\n" - f" ```{hangman_drawing}```" - ), + description=f"```{hangman_drawing}```", ) + game_data = self.games.get(ctx_or_interaction.channel.id) + if game.failed: embed.color = discord.Color.red() footer_text = f"Game over! The word was `{game.word}`!" @@ -400,12 +418,27 @@ async def generate_game_embed( footer_text = "Word guessed! Nice job!" else: embed.color = discord.Color.gold() - footer_text = f"{game.FINAL_STEP - game.step} wrong guesses left!" + embed.add_field( + name=f"Remaining Guesses {str(game.remaining_guesses())}", + value="\u200b", + inline=False + ) + embed.add_field( + name="Guessed Letters", + value=", ".join(game.guesses) or "None", + inline=False + ) - embed.set_footer(text=footer_text) + # Determine the game creator based on interaction type + if isinstance(ctx_or_interaction, discord.Interaction): + footer_text = f"Game started by {ctx_or_interaction.user}" + elif isinstance(ctx_or_interaction, commands.Context): + footer_text = f"Game started by {ctx_or_interaction.author}" + embed.set_footer(text=footer_text) return embed + @hangman.command(name="redraw", description="Redraws the current hangman game") async def redraw(self: Self, ctx: commands.Context) -> None: """A discord command to make a new embed with a new drawing of the running hangman game @@ -471,3 +504,56 @@ async def stop(self: Self, ctx: commands.Context) -> None: message=f"That game is now finished. The word was: `{word}`", channel=ctx.channel, ) + + @hangman.command( + name="add_guesses", + description="Allows the creator of the game to give more guesses", + usage="[number_of_guesses]", + ) + async def add_guesses( + self: Self, ctx: commands.Context, number_of_guesses: int + ) -> None: + """Discord command to allow the game creator to add more guesses. + + Args: + ctx (commands.Context): The context in which the command was run. + number_of_guesses (int): The number of guesses to add. + """ + if number_of_guesses <= 0: + await auxiliary.send_deny_embed( + message="The number of guesses must be a positive integer.", + channel=ctx.channel, + ) + return + + game_data = self.games.get(ctx.channel.id) + if not game_data: + await auxiliary.send_deny_embed( + message="There is no game in progress for this channel.", + channel=ctx.channel, + ) + return + + # Ensure only the creator of the game can add guesses + game_author = game_data.get("user") + if ctx.author.id != game_author.id: + await auxiliary.send_deny_embed( + message="Only the creator of the game can add more guesses.", + channel=ctx.channel, + ) + return + + game = game_data.get("game") + + # Add the new guesses + game.add_guesses(number_of_guesses) + + # Notify the channel + await ctx.send( + content=f"{number_of_guesses} guesses have been added! Total guesses remaining: {game.remaining_guesses()}" + ) + + # Update the game embed + embed = await self.generate_game_embed(ctx, game) + message = game_data.get("message") + await message.edit(embed=embed) From 2216d9c7102e1e7ceacf696e6a16cc9c31de5f04 Mon Sep 17 00:00:00 2001 From: TheKrol Date: Wed, 25 Dec 2024 09:18:21 -0500 Subject: [PATCH 02/17] Formatting update --- techsupport_bot/commands/hangman.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/techsupport_bot/commands/hangman.py b/techsupport_bot/commands/hangman.py index b2ae40bc..fec3ada9 100644 --- a/techsupport_bot/commands/hangman.py +++ b/techsupport_bot/commands/hangman.py @@ -276,7 +276,7 @@ async def hangman(self: Self, ctx: commands.Context) -> None: await auxiliary.extension_help(self, ctx, self.__module__[9:]) @app_commands.command( - name="start_hangman", + name="start_hangman", description="Start a Hangman game in the current channel.", extras={"module": "hangman"}, ) @@ -397,7 +397,9 @@ async def guess(self: Self, ctx: commands.Context, letter: str) -> None: await ctx.send(content=content) async def generate_game_embed( - self: Self, ctx_or_interaction: Union[discord.Interaction, commands.Context], game: HangmanGame + self: Self, + ctx_or_interaction: Union[discord.Interaction, commands.Context], + game: HangmanGame, ) -> discord.Embed: """Generate an embed for the current game state.""" hangman_drawing = game.draw_hang_state() @@ -409,7 +411,7 @@ async def generate_game_embed( ) game_data = self.games.get(ctx_or_interaction.channel.id) - + if game.failed: embed.color = discord.Color.red() footer_text = f"Game over! The word was `{game.word}`!" @@ -421,12 +423,12 @@ async def generate_game_embed( embed.add_field( name=f"Remaining Guesses {str(game.remaining_guesses())}", value="\u200b", - inline=False + inline=False, ) embed.add_field( - name="Guessed Letters", + name="Guessed Letters", value=", ".join(game.guesses) or "None", - inline=False + inline=False, ) # Determine the game creator based on interaction type @@ -438,7 +440,6 @@ async def generate_game_embed( embed.set_footer(text=footer_text) return embed - @hangman.command(name="redraw", description="Redraws the current hangman game") async def redraw(self: Self, ctx: commands.Context) -> None: """A discord command to make a new embed with a new drawing of the running hangman game From 319a44027603a5d7e1a81c748e7f9b3b95b840c9 Mon Sep 17 00:00:00 2001 From: TheKrol Date: Wed, 25 Dec 2024 11:32:11 -0500 Subject: [PATCH 03/17] Formatting and fixing the issue for help --- techsupport_bot/commands/hangman.py | 40 ++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/techsupport_bot/commands/hangman.py b/techsupport_bot/commands/hangman.py index fec3ada9..911fee1c 100644 --- a/techsupport_bot/commands/hangman.py +++ b/techsupport_bot/commands/hangman.py @@ -4,7 +4,7 @@ import datetime import uuid -from typing import TYPE_CHECKING, Self, Union +from typing import TYPE_CHECKING, Self import discord import ui @@ -110,6 +110,25 @@ class HangmanGame: ] def __init__(self: Self, word: str, max_guesses: int = 6) -> None: + """ + Initializes a new HangmanGame instance. + + Args: + word (str): The word to be guessed in the game. Must consist of only alphabetic characters + and cannot contain underscores. + max_guesses (int, optional): The maximum number of incorrect guesses allowed. Defaults to 6. + + Raises: + ValueError: If the provided word is empty, contains underscores, or has non-alphabetic characters. + + Attributes: + word (str): The word to be guessed in the game. + guesses (set): A set containing all the guessed letters. + step (int): The current step or state of the game, representing incorrect guesses made. + max_guesses (int): The maximum number of incorrect guesses allowed. + started (datetime.datetime): The UTC timestamp when the game was started. + id (uuid.UUID): A unique identifier for this game instance. + """ if not word or "_" in word or not word.isalpha(): raise ValueError("valid word must be provided") self.word = word @@ -340,7 +359,7 @@ async def start_game( } await interaction.followup.send( - f"The Hangman game has started with a hidden word!", ephemeral=True + "The Hangman game has started with a hidden word!", ephemeral=True ) @hangman.command( @@ -398,20 +417,22 @@ async def guess(self: Self, ctx: commands.Context, letter: str) -> None: async def generate_game_embed( self: Self, - ctx_or_interaction: Union[discord.Interaction, commands.Context], + ctx_or_interaction: discord.Interaction | commands.Context, game: HangmanGame, ) -> discord.Embed: """Generate an embed for the current game state.""" hangman_drawing = game.draw_hang_state() hangman_word = game.draw_word_state() + prefix = await self.bot.get_prefix(ctx_or_interaction.message) embed = discord.Embed( title=f"`{hangman_word}`", - description=f"```{hangman_drawing}```", + description=( + f"Type `{prefix}help hangman` for more info\n\n" + f"```{hangman_drawing}```", + ), ) - game_data = self.games.get(ctx_or_interaction.channel.id) - if game.failed: embed.color = discord.Color.red() footer_text = f"Game over! The word was `{game.word}`!" @@ -436,6 +457,8 @@ async def generate_game_embed( footer_text = f"Game started by {ctx_or_interaction.user}" elif isinstance(ctx_or_interaction, commands.Context): footer_text = f"Game started by {ctx_or_interaction.author}" + else: + footer_text = " " embed.set_footer(text=footer_text) return embed @@ -551,7 +574,10 @@ async def add_guesses( # Notify the channel await ctx.send( - content=f"{number_of_guesses} guesses have been added! Total guesses remaining: {game.remaining_guesses()}" + content=( + f"{number_of_guesses} guesses have been added! " + f"Total guesses remaining: {game.remaining_guesses()}" + ) ) # Update the game embed From 64e5529bce708819ef0f936c1a89bcf6fe43b442 Mon Sep 17 00:00:00 2001 From: TheKrol Date: Wed, 25 Dec 2024 11:38:09 -0500 Subject: [PATCH 04/17] doc string update --- techsupport_bot/commands/hangman.py | 44 ++++++++++++----------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/techsupport_bot/commands/hangman.py b/techsupport_bot/commands/hangman.py index 911fee1c..faf4a0a9 100644 --- a/techsupport_bot/commands/hangman.py +++ b/techsupport_bot/commands/hangman.py @@ -36,18 +36,29 @@ async def setup(bot: bot.TechSupportBot) -> None: class HangmanGame: - """Class for the game hangman. + """ + A class that represents a game of Hangman. + + The game includes the logic for tracking the word to be guessed, the guesses made, + and the state of the hangman figure based on incorrect guesses. It also supports + additional functionality such as adding more guesses and determining whether the game + is finished or failed. Attributes: - HANG_PICS (list[str]): The list of hangman pictures - finished (bool): Determines if the game has been finished or not - failed (bool): Determines if the players failed to guess the word + HANG_PICS (list[str]): The list of hangman pictures. + word (str): The word that players need to guess. + guesses (set): A set of guessed letters. + step (int): The current number of incorrect guesses made. + max_guesses (int): The maximum number of incorrect guesses allowed before the game ends. + started (datetime): The UTC timestamp of when the game was started. + id (UUID): A unique identifier for the game. Args: - word (str): The word to start the game with + word (str): The word for the game. It must be an alphabetic string without underscores. + max_guesses (int, optional): The maximum number of incorrect guesses allowed. Raises: - ValueError: A valid alphabetic word wasn't provided + ValueError: A valid alphabetic word wasn't provided. """ HANG_PICS: list[str] = [ @@ -109,26 +120,7 @@ class HangmanGame: =========""", ] - def __init__(self: Self, word: str, max_guesses: int = 6) -> None: - """ - Initializes a new HangmanGame instance. - - Args: - word (str): The word to be guessed in the game. Must consist of only alphabetic characters - and cannot contain underscores. - max_guesses (int, optional): The maximum number of incorrect guesses allowed. Defaults to 6. - - Raises: - ValueError: If the provided word is empty, contains underscores, or has non-alphabetic characters. - - Attributes: - word (str): The word to be guessed in the game. - guesses (set): A set containing all the guessed letters. - step (int): The current step or state of the game, representing incorrect guesses made. - max_guesses (int): The maximum number of incorrect guesses allowed. - started (datetime.datetime): The UTC timestamp when the game was started. - id (uuid.UUID): A unique identifier for this game instance. - """ + def __init__(self: Self, word: str, max_guesses: int = 6) -> None: if not word or "_" in word or not word.isalpha(): raise ValueError("valid word must be provided") self.word = word From 89303ab5de24b863133bd8a48dda3559ade8e4d3 Mon Sep 17 00:00:00 2001 From: TheKrol Date: Wed, 25 Dec 2024 11:39:54 -0500 Subject: [PATCH 05/17] Trailing white space removed --- techsupport_bot/commands/hangman.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/commands/hangman.py b/techsupport_bot/commands/hangman.py index faf4a0a9..ee8872ee 100644 --- a/techsupport_bot/commands/hangman.py +++ b/techsupport_bot/commands/hangman.py @@ -120,7 +120,7 @@ class HangmanGame: =========""", ] - def __init__(self: Self, word: str, max_guesses: int = 6) -> None: + def __init__(self: Self, word: str, max_guesses: int = 6) -> None: if not word or "_" in word or not word.isalpha(): raise ValueError("valid word must be provided") self.word = word From 27f59bf485c202839364397a86fb0e9415134ff2 Mon Sep 17 00:00:00 2001 From: TheKrol Date: Wed, 25 Dec 2024 12:57:03 -0500 Subject: [PATCH 06/17] Doc string update --- techsupport_bot/commands/hangman.py | 63 +++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/techsupport_bot/commands/hangman.py b/techsupport_bot/commands/hangman.py index ee8872ee..9d8d8fb8 100644 --- a/techsupport_bot/commands/hangman.py +++ b/techsupport_bot/commands/hangman.py @@ -183,7 +183,16 @@ def guess(self: Self, letter: str) -> bool: @property def finished(self: Self) -> bool: - """Method to finish the game of hangman.""" + """ + Determines if the game of Hangman is finished. + + The game is considered finished in the following scenarios: + - The number of incorrect guesses (`step`) exceeds or equals the maximum allowed (`max_guesses`). + - All the letters in the word have been correctly guessed. + + Returns: + bool: True if the game is finished (either won or lost), False otherwise. + """ if self.step < 0 or self.step >= self.max_guesses: return True if all(letter in self.guesses for letter in self.word): @@ -192,13 +201,23 @@ def finished(self: Self) -> bool: @property def failed(self: Self) -> bool: - """Method in case the game wasn't successful.""" + """ + Determines if the game was unsuccessful. + + The game is considered a failure when the number of incorrect guesses (`step`) + equals or exceeds the maximum allowed (`max_guesses`), meaning the players + failed to guess the word within the allowed attempts. + + Returns: + bool: True if the game was unsuccessful (players lost), False otherwise. + """ if self.step >= self.max_guesses: return True return False def guessed(self: Self, letter: str) -> bool: - """Method to know if a letter has already been guessed + """ + Method to know if a letter has already been guessed Args: letter (str): The letter to check if it has been guessed @@ -217,16 +236,31 @@ def guessed(self: Self, letter: str) -> bool: return False def remaining_guesses(self) -> int: - """Returns the number of guesses remaining.""" + """ + Calculates the number of guesses remaining in the game. + + The remaining guesses are determined by subtracting the number of incorrect + guesses (`step`) from the maximum allowed guesses (`max_guesses`). + + Returns: + int: The number of guesses the players have left. + """ return self.max_guesses - self.step def add_guesses(self, num_guesses: int) -> None: - """Adds more guesses to the game.""" + """ + Increases the total number of allowed guesses in the game. + + Args: + num_guesses (int): The number of additional guesses to add to the + current maximum allowed guesses. + """ self.max_guesses += num_guesses async def can_stop_game(ctx: commands.Context) -> bool: - """Checks if a user has the ability to stop the running game + """ + Checks if a user has the ability to stop the running game Args: ctx (commands.Context): The context in which the stop command was run @@ -412,7 +446,22 @@ async def generate_game_embed( ctx_or_interaction: discord.Interaction | commands.Context, game: HangmanGame, ) -> discord.Embed: - """Generate an embed for the current game state.""" + """ + Generates an embed representing the current state of the Hangman game. + + Args: + ctx_or_interaction (discord.Interaction | commands.Context): + The context or interaction used to generate the embed, which provides + information about the user and the message. + game (HangmanGame): The current instance of the Hangman game, used to + retrieve game state, including word state, remaining guesses, and the + hangman drawing. + + Returns: + discord.Embed: An embed displaying the current game state, including + the hangman drawing, word state, remaining guesses, guessed letters, + and the footer indicating the game status and creator. + """ hangman_drawing = game.draw_hang_state() hangman_word = game.draw_word_state() prefix = await self.bot.get_prefix(ctx_or_interaction.message) From cd04c6e6afd8dcce57cb5a609819e8a1de3eaac3 Mon Sep 17 00:00:00 2001 From: TheKrol Date: Wed, 25 Dec 2024 12:58:20 -0500 Subject: [PATCH 07/17] Format and small update --- techsupport_bot/commands/hangman.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/techsupport_bot/commands/hangman.py b/techsupport_bot/commands/hangman.py index 9d8d8fb8..4d54f5a7 100644 --- a/techsupport_bot/commands/hangman.py +++ b/techsupport_bot/commands/hangman.py @@ -187,7 +187,7 @@ def finished(self: Self) -> bool: Determines if the game of Hangman is finished. The game is considered finished in the following scenarios: - - The number of incorrect guesses (`step`) exceeds or equals the maximum allowed (`max_guesses`). + - The number of incorrect guesses exceeds or equals the maximum allowed. - All the letters in the word have been correctly guessed. Returns: @@ -204,8 +204,8 @@ def failed(self: Self) -> bool: """ Determines if the game was unsuccessful. - The game is considered a failure when the number of incorrect guesses (`step`) - equals or exceeds the maximum allowed (`max_guesses`), meaning the players + The game is considered a failure when the number of incorrect guesses (`step`) + equals or exceeds the maximum allowed (`max_guesses`), meaning the players failed to guess the word within the allowed attempts. Returns: @@ -239,7 +239,7 @@ def remaining_guesses(self) -> int: """ Calculates the number of guesses remaining in the game. - The remaining guesses are determined by subtracting the number of incorrect + The remaining guesses are determined by subtracting the number of incorrect guesses (`step`) from the maximum allowed guesses (`max_guesses`). Returns: @@ -252,7 +252,7 @@ def add_guesses(self, num_guesses: int) -> None: Increases the total number of allowed guesses in the game. Args: - num_guesses (int): The number of additional guesses to add to the + num_guesses (int): The number of additional guesses to add to the current maximum allowed guesses. """ self.max_guesses += num_guesses @@ -450,16 +450,16 @@ async def generate_game_embed( Generates an embed representing the current state of the Hangman game. Args: - ctx_or_interaction (discord.Interaction | commands.Context): - The context or interaction used to generate the embed, which provides + ctx_or_interaction (discord.Interaction | commands.Context): + The context or interaction used to generate the embed, which provides information about the user and the message. - game (HangmanGame): The current instance of the Hangman game, used to - retrieve game state, including word state, remaining guesses, and the + game (HangmanGame): The current instance of the Hangman game, used to + retrieve game state, including word state, remaining guesses, and the hangman drawing. Returns: - discord.Embed: An embed displaying the current game state, including - the hangman drawing, word state, remaining guesses, guessed letters, + discord.Embed: An embed displaying the current game state, including + the hangman drawing, word state, remaining guesses, guessed letters, and the footer indicating the game status and creator. """ hangman_drawing = game.draw_hang_state() From a58834a1b79c27f3754a38b05461b3d55322b83a Mon Sep 17 00:00:00 2001 From: TheKrol Date: Wed, 25 Dec 2024 14:14:25 -0500 Subject: [PATCH 08/17] Flake update --- techsupport_bot/commands/hangman.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/techsupport_bot/commands/hangman.py b/techsupport_bot/commands/hangman.py index 4d54f5a7..d68a76b4 100644 --- a/techsupport_bot/commands/hangman.py +++ b/techsupport_bot/commands/hangman.py @@ -186,9 +186,10 @@ def finished(self: Self) -> bool: """ Determines if the game of Hangman is finished. - The game is considered finished in the following scenarios: - - The number of incorrect guesses exceeds or equals the maximum allowed. - - All the letters in the word have been correctly guessed. + The game is considered finished if: + - The number of incorrect guesses (`step`) is greater than or + equal to the maximum allowed (`max_guesses`). + - All letters in the word have been correctly guessed, meaning the game has been won. Returns: bool: True if the game is finished (either won or lost), False otherwise. @@ -205,16 +206,18 @@ def failed(self: Self) -> bool: Determines if the game was unsuccessful. The game is considered a failure when the number of incorrect guesses (`step`) - equals or exceeds the maximum allowed (`max_guesses`), meaning the players + equals or exceeds the maximum allowed guesses (`max_guesses`), meaning the players failed to guess the word within the allowed attempts. Returns: - bool: True if the game was unsuccessful (players lost), False otherwise. + bool: True if the game was unsuccessful (i.e., the number of incorrect guesses + is greater than or equal to the maximum allowed), False otherwise. """ if self.step >= self.max_guesses: return True return False + def guessed(self: Self, letter: str) -> bool: """ Method to know if a letter has already been guessed @@ -235,7 +238,7 @@ def guessed(self: Self, letter: str) -> bool: return True return False - def remaining_guesses(self) -> int: + def remaining_guesses(self: Self) -> int: """ Calculates the number of guesses remaining in the game. @@ -247,13 +250,16 @@ def remaining_guesses(self) -> int: """ return self.max_guesses - self.step - def add_guesses(self, num_guesses: int) -> None: + def add_guesses(self: Self, num_guesses: int) -> None: """ Increases the total number of allowed guesses in the game. Args: - num_guesses (int): The number of additional guesses to add to the - current maximum allowed guesses. + num_guesses (int): The number of additional guesses to add to the + current maximum allowed guesses. + + Returns: + None """ self.max_guesses += num_guesses From 194275ef7a6d47c5c1c7a8eb628f27647f6ab981 Mon Sep 17 00:00:00 2001 From: TheKrol Date: Wed, 25 Dec 2024 14:25:26 -0500 Subject: [PATCH 09/17] Flake update --- techsupport_bot/commands/hangman.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/techsupport_bot/commands/hangman.py b/techsupport_bot/commands/hangman.py index d68a76b4..663d18ca 100644 --- a/techsupport_bot/commands/hangman.py +++ b/techsupport_bot/commands/hangman.py @@ -52,6 +52,8 @@ class HangmanGame: max_guesses (int): The maximum number of incorrect guesses allowed before the game ends. started (datetime): The UTC timestamp of when the game was started. id (UUID): A unique identifier for the game. + finished (bool): Determines if the game has been finished or not + failed (bool): Determines if the players failed to guess the word Args: word (str): The word for the game. It must be an alphabetic string without underscores. @@ -187,7 +189,7 @@ def finished(self: Self) -> bool: Determines if the game of Hangman is finished. The game is considered finished if: - - The number of incorrect guesses (`step`) is greater than or + - The number of incorrect guesses (`step`) is greater than or equal to the maximum allowed (`max_guesses`). - All letters in the word have been correctly guessed, meaning the game has been won. @@ -217,7 +219,6 @@ def failed(self: Self) -> bool: return True return False - def guessed(self: Self, letter: str) -> bool: """ Method to know if a letter has already been guessed @@ -255,11 +256,8 @@ def add_guesses(self: Self, num_guesses: int) -> None: Increases the total number of allowed guesses in the game. Args: - num_guesses (int): The number of additional guesses to add to the + num_guesses (int): The number of additional guesses to add to the current maximum allowed guesses. - - Returns: - None """ self.max_guesses += num_guesses From bfc467a3a1c669063cbbb6a8e588e2937f4d85d5 Mon Sep 17 00:00:00 2001 From: TheKrol Date: Wed, 19 Feb 2025 02:56:34 -0500 Subject: [PATCH 10/17] Fix game start error --- techsupport_bot/commands/hangman.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/techsupport_bot/commands/hangman.py b/techsupport_bot/commands/hangman.py index 663d18ca..de060c33 100644 --- a/techsupport_bot/commands/hangman.py +++ b/techsupport_bot/commands/hangman.py @@ -306,8 +306,9 @@ async def can_stop_game(ctx: commands.Context) -> bool: class HangmanCog(cogs.BaseCog): """Class to define the hangman game.""" - async def preconfig(self: Self) -> None: - """Method to preconfig the game.""" + def __init__(self, bot): + """Initialize the HangmanCog.""" + super().__init__(bot) self.games = {} @commands.guild_only() @@ -468,13 +469,24 @@ async def generate_game_embed( """ hangman_drawing = game.draw_hang_state() hangman_word = game.draw_word_state() - prefix = await self.bot.get_prefix(ctx_or_interaction.message) + # Determine the guild ID + guild_id = None + if isinstance(ctx_or_interaction, commands.Context): + guild_id = ctx_or_interaction.guild.id if ctx_or_interaction.guild else None + elif isinstance(ctx_or_interaction, discord.Interaction): + guild_id = ctx_or_interaction.guild_id + + # Fetch the prefix manually since get_prefix expects a Message + if guild_id and str(guild_id) in self.bot.guild_configs: + prefix = self.bot.guild_configs[str(guild_id)].command_prefix + else: + prefix = self.file_config.bot_config.default_prefix embed = discord.Embed( title=f"`{hangman_word}`", description=( f"Type `{prefix}help hangman` for more info\n\n" - f"```{hangman_drawing}```", + f"```{hangman_drawing}```" ), ) From 9e4eadf5a86d19ba2140927ab135825090f6bd20 Mon Sep 17 00:00:00 2001 From: TheKrol Date: Wed, 19 Feb 2025 02:59:37 -0500 Subject: [PATCH 11/17] Flake update --- techsupport_bot/commands/hangman.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/techsupport_bot/commands/hangman.py b/techsupport_bot/commands/hangman.py index de060c33..7b1fab30 100644 --- a/techsupport_bot/commands/hangman.py +++ b/techsupport_bot/commands/hangman.py @@ -304,10 +304,13 @@ async def can_stop_game(ctx: commands.Context) -> bool: class HangmanCog(cogs.BaseCog): - """Class to define the hangman game.""" + """Class to define the hangman game. + + Args: + bot (commands.Bot): The bot instance that this cog is a part of. + """ def __init__(self, bot): - """Initialize the HangmanCog.""" super().__init__(bot) self.games = {} From 81c3eb077c4d2938cdc0ec0721a3e4a7254c1aff Mon Sep 17 00:00:00 2001 From: TheKrol Date: Wed, 19 Feb 2025 03:02:09 -0500 Subject: [PATCH 12/17] More Flake --- techsupport_bot/commands/hangman.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/techsupport_bot/commands/hangman.py b/techsupport_bot/commands/hangman.py index 7b1fab30..30eb5b8a 100644 --- a/techsupport_bot/commands/hangman.py +++ b/techsupport_bot/commands/hangman.py @@ -311,6 +311,12 @@ class HangmanCog(cogs.BaseCog): """ def __init__(self, bot): + """Initialize the HangmanCog with the given bot instance. + + Args: + bot (commands.Bot): The bot instance that this cog is a part of. + """ + super().__init__(bot) self.games = {} From 2dfef8f374b2ac13e567933ade5b59c1b07eb790 Mon Sep 17 00:00:00 2001 From: TheKrol Date: Wed, 19 Feb 2025 03:09:08 -0500 Subject: [PATCH 13/17] More Flake --- techsupport_bot/commands/hangman.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/techsupport_bot/commands/hangman.py b/techsupport_bot/commands/hangman.py index 30eb5b8a..4f692294 100644 --- a/techsupport_bot/commands/hangman.py +++ b/techsupport_bot/commands/hangman.py @@ -310,13 +310,7 @@ class HangmanCog(cogs.BaseCog): bot (commands.Bot): The bot instance that this cog is a part of. """ - def __init__(self, bot): - """Initialize the HangmanCog with the given bot instance. - - Args: - bot (commands.Bot): The bot instance that this cog is a part of. - """ - + def __init__(self: Self, bot: commands.Bot) -> None: super().__init__(bot) self.games = {} From 50d6317383f22856906f38b2c0849648f0055d17 Mon Sep 17 00:00:00 2001 From: TheKrol Date: Wed, 19 Feb 2025 04:03:41 -0500 Subject: [PATCH 14/17] Fixed naming --- techsupport_bot/commands/hangman.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/techsupport_bot/commands/hangman.py b/techsupport_bot/commands/hangman.py index 4f692294..c992d376 100644 --- a/techsupport_bot/commands/hangman.py +++ b/techsupport_bot/commands/hangman.py @@ -328,8 +328,12 @@ async def hangman(self: Self, ctx: commands.Context) -> None: # Executed if there are no/invalid args supplied await auxiliary.extension_help(self, ctx, self.__module__[9:]) - @app_commands.command( - name="start_hangman", + hangman_app_group: app_commands.Group = app_commands.Group( + name="hangman", description="Command Group for the Hangman Extension" + ) + + @hangman_app_group.command( + name="start", description="Start a Hangman game in the current channel.", extras={"module": "hangman"}, ) From 3079cc232a53268020864adac50e86eb89cdc058 Mon Sep 17 00:00:00 2001 From: TheKrol Date: Wed, 19 Feb 2025 04:07:24 -0500 Subject: [PATCH 15/17] Flake update --- techsupport_bot/commands/hangman.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/techsupport_bot/commands/hangman.py b/techsupport_bot/commands/hangman.py index c992d376..3265b071 100644 --- a/techsupport_bot/commands/hangman.py +++ b/techsupport_bot/commands/hangman.py @@ -304,10 +304,15 @@ async def can_stop_game(ctx: commands.Context) -> bool: class HangmanCog(cogs.BaseCog): - """Class to define the hangman game. + """Class to define the Hangman game. Args: bot (commands.Bot): The bot instance that this cog is a part of. + + Attributes: + games (dict): A dictionary to store ongoing games, where the keys are + player identifiers and the values are the current game state. + hangman_app_group (app_commands.Group): The command group for the Hangman extension. """ def __init__(self: Self, bot: commands.Bot) -> None: @@ -326,7 +331,7 @@ async def hangman(self: Self, ctx: commands.Context) -> None: """ # Executed if there are no/invalid args supplied - await auxiliary.extension_help(self, ctx, self.__module__[9:]) + await auxiliary.extension_help(self, ctx, module=self.__module__[9:]) hangman_app_group: app_commands.Group = app_commands.Group( name="hangman", description="Command Group for the Hangman Extension" From 648ebad54db56e644e261f6c6752828246ff5214 Mon Sep 17 00:00:00 2001 From: TheKrol Date: Wed, 19 Feb 2025 10:40:54 -0500 Subject: [PATCH 16/17] Fix issues and added a check for word length --- techsupport_bot/commands/hangman.py | 33 ++++++++++++++++++----------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/techsupport_bot/commands/hangman.py b/techsupport_bot/commands/hangman.py index 3265b071..8e5175dd 100644 --- a/techsupport_bot/commands/hangman.py +++ b/techsupport_bot/commands/hangman.py @@ -282,6 +282,9 @@ async def can_stop_game(ctx: commands.Context) -> bool: raise AttributeError("could not find hangman cog when checking game states") game_data = cog.games.get(ctx.channel.id) + if not game_data: + return True + user = game_data.get("user") if getattr(user, "id", 0) == ctx.author.id: return True @@ -331,7 +334,7 @@ async def hangman(self: Self, ctx: commands.Context) -> None: """ # Executed if there are no/invalid args supplied - await auxiliary.extension_help(self, ctx, module=self.__module__[9:]) + await auxiliary.extension_help(self, ctx, self.__module__[9:]) hangman_app_group: app_commands.Group = app_commands.Group( name="hangman", description="Command Group for the Hangman Extension" @@ -354,6 +357,13 @@ async def start_game( # Ensure only the command's author can see this interaction await interaction.response.defer(ephemeral=True) + # Check if the provided word is too long + if len(word) >= 256: + await interaction.followup.send( + "The word must be less than 256 characters.", ephemeral=True + ) + return + # Check if a game is already active in the channel game_data = self.games.get(interaction.channel_id) if game_data: @@ -418,29 +428,27 @@ async def guess(self: Self, ctx: commands.Context, letter: str) -> None: letter (str): The letter the user is trying to guess """ game_data = self.games.get(ctx.channel.id) - if ctx.author == game_data.get("user"): + if not game_data: await auxiliary.send_deny_embed( - message="You cannot guess letters because you started this game!", + message="There is no game in progress for this channel", channel=ctx.channel, ) return - if len(letter) > 1 or not letter.isalpha(): + if ctx.author == game_data.get("user"): await auxiliary.send_deny_embed( - message="You can only guess a letter", channel=ctx.channel + message="You cannot guess letters because you started this game!", + channel=ctx.channel, ) return - game_data = self.games.get(ctx.channel.id) - if not game_data: + if len(letter) > 1 or not letter.isalpha(): await auxiliary.send_deny_embed( - message="There is no game in progress for this channel", - channel=ctx.channel, + message="You can only guess a letter", channel=ctx.channel ) return game = game_data.get("game") - if game.guessed(letter): await auxiliary.send_deny_embed( message="That letter has already been guessed", channel=ctx.channel @@ -570,7 +578,8 @@ async def stop(self: Self, ctx: commands.Context) -> None: game_data = self.games.get(ctx.channel.id) if not game_data: await auxiliary.send_deny_embed( - "There is no game in progress for this channel", channel=ctx.channel + message="There is no game in progress for this channel", + channel=ctx.channel, ) return @@ -622,7 +631,7 @@ async def add_guesses( game_data = self.games.get(ctx.channel.id) if not game_data: await auxiliary.send_deny_embed( - message="There is no game in progress for this channel.", + message="There is no game in progress for this channel", channel=ctx.channel, ) return From 10603384e9c7d0ec150735dcbaff44309a008ce9 Mon Sep 17 00:00:00 2001 From: TheKrol Date: Wed, 19 Feb 2025 10:49:20 -0500 Subject: [PATCH 17/17] Update length for starting word --- techsupport_bot/commands/hangman.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/commands/hangman.py b/techsupport_bot/commands/hangman.py index 8e5175dd..fc599db2 100644 --- a/techsupport_bot/commands/hangman.py +++ b/techsupport_bot/commands/hangman.py @@ -358,7 +358,7 @@ async def start_game( await interaction.response.defer(ephemeral=True) # Check if the provided word is too long - if len(word) >= 256: + if len(word) >= 85: await interaction.followup.send( "The word must be less than 256 characters.", ephemeral=True )