From 874326c4bb05ad36224e388d134691c55cd2141a Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 19 Sep 2021 00:45:36 -0400 Subject: Hangman (#843) * beginning commit creating the base of the hangman, code needs to be linted in the future * updated words list * adding images to show the hangman person * added images, though it is a bit laggy * replacing images with discord attachment urls * adding error if filters aren't found * fixing typo in ``filter_not_found_embed`` * final lints + removing `mode` parameter as it renders useless * linting flake8 errors * adding newline at the end of `top_1000_used_words.txt` * minor change to filter message * improving hangman docstring * removing `bot/resources/evergreen/hangman` directory as file attachments are used * replacing single quotes with double quotes, to adhere to the style guide. * fixing style inconsistencies and other problems with how the code looks - as per requested by Objectivix * fixing `IMAGES` style inconsistency * adding trailing commas and switching to `Colours` for consistency * adding trailing commas and switching to `Colours` for consistency * fixing the remnants of non-trailing commas and allowing specification for single player vs mulitplayer * removing all 2 letter words from the hangman word choosing and removing words that @Objectivix found that shouldn't be in the list of words * removing some inappropriate words from the txt file * Adding space for grammatical errors Co-authored-by: ChrisJL * changing two periods to a full stop & wrapping try and except block to only the part that can raise it * using negative replies instead along with fixing grammatical errors in the sentence * removing words that could be considered inappropirate * removing `TOP_WORDS_FILE_PATH` and making `ALL_WORDS` a global variable. * more specific docstring * more specific docstring * Removing more words The words removed shouldn't really belong here * replacing mapping_of_images with IMAGES and other fixes * Dedenting Co-authored-by: Bluenix * Improving tries logic Co-authored-by: Bluenix * Updating `positions` list to set Co-authored-by: Bluenix * fixing too many blank lines * Hardcode dictionary Co-authored-by: Bluenix * remove 3 letter words * add the word python * remove all 3 letter words - forgot to remove some * case insensitivity * changes to improve gameplay * setting check outside of every iteration * checking if a letter has already been guessed * changing to transparent images without the shadows * consistency with timeout * capitalization / edits to the hangman_words.txt * changing `singleplayer` to a boolean * sending then deleting, along with encouraging to try again * Grammar Co-authored-by: Bluenix * Grammatical error Co-authored-by: Bluenix * Simplification Co-authored-by: ChrisJL * changing from pathlib to open * python-related words * two more python-related words * making error embeds more clear * Update hangman_words.txt deleted a possibly inappropriate word and added 3 new python related words * Update hangman.py Added some more comments and made some line spacing changes before and after the docstring * adding a new word * Adding newline * updating comments * when the game has won, it will display the word * add helper function to abstract some code, and edit the message at the end when won with the original word * editing message for win screen for consistency * prettifying the user guess * sending win and losing embed separately * Clarify 'tries remaining' Co-authored-by: ChrisJL * changing to `delete_after` * not editing `message.content` variable * fixing error; changing to len(normalized_content) * Update hangman.py Reworded the comment about the timeout a little * last nitpicks for grammatical errors in comments * adding suggestions from ToxicKidz * Improving comments/removing unnecessary ones Co-authored-by: Bluenix * Renaming parameter from `singleplayer` to `mode` Co-authored-by: Bluenix Co-authored-by: ChrisJL Co-authored-by: Bluenix Co-authored-by: DMFriends --- bot/exts/fun/hangman.py | 194 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 bot/exts/fun/hangman.py (limited to 'bot/exts/fun/hangman.py') diff --git a/bot/exts/fun/hangman.py b/bot/exts/fun/hangman.py new file mode 100644 index 00000000..08883103 --- /dev/null +++ b/bot/exts/fun/hangman.py @@ -0,0 +1,194 @@ +from asyncio import TimeoutError +from pathlib import Path +from random import choice +from typing import Literal + +from discord import Embed, Message +from discord.ext import commands + +from bot.bot import Bot +from bot.constants import Colours, NEGATIVE_REPLIES + +# Defining all words in the list of words as a global variable +ALL_WORDS = Path("bot/resources/fun/hangman_words.txt").read_text().splitlines() + +# Defining a dictionary of images that will be used for the game to represent the hangman person +IMAGES = { + 6: "https://cdn.discordapp.com/attachments/859123972884922418/888133201497837598/hangman0.png", + 5: "https://cdn.discordapp.com/attachments/859123972884922418/888133595259084800/hangman1.png", + 4: "https://cdn.discordapp.com/attachments/859123972884922418/888134194474139688/hangman2.png", + 3: "https://cdn.discordapp.com/attachments/859123972884922418/888133758069395466/hangman3.png", + 2: "https://cdn.discordapp.com/attachments/859123972884922418/888133786724859924/hangman4.png", + 1: "https://cdn.discordapp.com/attachments/859123972884922418/888133828831477791/hangman5.png", + 0: "https://cdn.discordapp.com/attachments/859123972884922418/888133845449338910/hangman6.png", +} + + +class Hangman(commands.Cog): + """ + Cog for the Hangman game. + + Hangman is a classic game where the user tries to guess a word, with a limited amount of tries. + """ + + def __init__(self, bot: Bot): + self.bot = bot + + @staticmethod + def create_embed(tries: int, user_guess: str) -> Embed: + """ + Helper method that creates the embed where the game information is shown. + + This includes how many letters the user has guessed so far, and the hangman photo itself. + """ + hangman_embed = Embed( + title="Hangman", + color=Colours.python_blue, + ) + hangman_embed.set_image(url=IMAGES[tries]) + hangman_embed.add_field( + name=f"You've guessed `{user_guess}` so far.", + value="Guess the word by sending a message with a letter!" + ) + hangman_embed.set_footer(text=f"Tries remaining: {tries}") + return hangman_embed + + @commands.command() + async def hangman( + self, + ctx: commands.Context, + min_length: int = 0, + max_length: int = 25, + min_unique_letters: int = 0, + max_unique_letters: int = 25, + mode: Literal["s", "m", "S", "M"] = "s", + ) -> None: + """ + Play hangman against the bot, where you have to guess the word it has provided! + + The arguments for this command mean: + - min_length: the minimum length you want the word to be (i.e. 2) + - max_length: the maximum length you want the word to be (i.e. 5) + - min_unique_letters: the minimum unique letters you want the word to have (i.e. 4) + - max_unique_letters: the maximum unique letters you want the word to have (i.e. 7) + - mode: writing 's' means you want to play by yourself, and only you can suggest letters, + - writing 'm' means you want multiple players to join in and guess the word. + """ + # Changing singleplayer to a boolean + singleplayer = mode.lower() == 's' + + # Filtering the list of all words depending on the configuration + filtered_words = [ + word for word in ALL_WORDS + if min_length < len(word) < max_length + and min_unique_letters < len(set(word)) < max_unique_letters + ] + + if not filtered_words: + filter_not_found_embed = Embed( + title=choice(NEGATIVE_REPLIES), + description="No words could be found that fit all filters specified.", + color=Colours.soft_red, + ) + await ctx.send(embed=filter_not_found_embed) + return + + word = choice(filtered_words) + # `pretty_word` is used for comparing the indices where the guess of the user is similar to the word + # The `user_guess` variable is prettified by adding spaces between every dash, and so is the `pretty_word` + pretty_word = ''.join([f"{letter} " for letter in word])[:-1] + user_guess = ("_ " * len(word))[:-1] + tries = 6 + guessed_letters = set() + + # Checking if the game is singleplayer + def check(msg: Message) -> bool: + if singleplayer: + return msg.author == ctx.author + else: + # Multiplayer mode + return not msg.author.bot + + original_message = await ctx.send(embed=Embed( + title="Hangman", + description="Loading game...", + color=Colours.soft_green + )) + + # Game loop + while user_guess.replace(' ', '') != word: + # Edit the message to the current state of the game + await original_message.edit(embed=self.create_embed(tries, user_guess)) + + try: + message = await self.bot.wait_for( + event="message", + timeout=60.0, + check=check + ) + except TimeoutError: + timeout_embed = Embed( + title=choice(NEGATIVE_REPLIES), + description="Looks like the bot timed out! You must send a letter within 60 seconds.", + color=Colours.soft_red, + ) + await original_message.edit(embed=timeout_embed) + return + + # If the user enters a capital letter as their guess, it is automatically converted to a lowercase letter + normalized_content = message.content.lower() + # The user should only guess one letter per message + if len(normalized_content) > 1: + letter_embed = Embed( + title=choice(NEGATIVE_REPLIES), + description="You can only send one letter at a time, try again!", + color=Colours.dark_green, + ) + await ctx.send(embed=letter_embed, delete_after=4) + continue + + # Checks for repeated guesses + elif normalized_content in guessed_letters: + already_guessed_embed = Embed( + title=choice(NEGATIVE_REPLIES), + description=f"You have already guessed `{normalized_content}`, try again!", + color=Colours.dark_green, + ) + await ctx.send(embed=already_guessed_embed, delete_after=4) + continue + + # Checks for correct guesses from the user + elif normalized_content in word: + positions = {idx for idx, letter in enumerate(pretty_word) if letter == normalized_content} + user_guess = "".join( + [normalized_content if index in positions else dash for index, dash in enumerate(user_guess)] + ) + + else: + tries -= 1 + + if tries <= 0: + losing_embed = Embed( + title="You lost.", + description=f"The word was `{word}`.", + color=Colours.soft_red, + ) + await original_message.edit(embed=self.create_embed(tries, user_guess)) + await ctx.send(embed=losing_embed) + return + + guessed_letters.add(normalized_content) + + # The loop exited meaning that the user has guessed the word + await original_message.edit(embed=self.create_embed(tries, user_guess)) + win_embed = Embed( + title="You won!", + description=f"The word was `{word}`.", + color=Colours.grass_green + ) + await ctx.send(embed=win_embed) + + +def setup(bot: Bot) -> None: + """Load the Hangman cog.""" + bot.add_cog(Hangman(bot)) -- cgit v1.2.3 From 1f5970134d710358f716c35d8d7e33b6f76f48d3 Mon Sep 17 00:00:00 2001 From: Bluenix Date: Sun, 19 Sep 2021 15:27:48 +0200 Subject: Remove multiplayer mode and correctly check current channel Closes #871 With multiplayer and a missing check for the right channel the bot would respond to each message as one directed to the game. Multiplayer mode is planned to be reintroduced later on using threads. --- bot/exts/fun/hangman.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) (limited to 'bot/exts/fun/hangman.py') diff --git a/bot/exts/fun/hangman.py b/bot/exts/fun/hangman.py index 08883103..db95ba5c 100644 --- a/bot/exts/fun/hangman.py +++ b/bot/exts/fun/hangman.py @@ -1,7 +1,6 @@ from asyncio import TimeoutError from pathlib import Path from random import choice -from typing import Literal from discord import Embed, Message from discord.ext import commands @@ -61,7 +60,6 @@ class Hangman(commands.Cog): max_length: int = 25, min_unique_letters: int = 0, max_unique_letters: int = 25, - mode: Literal["s", "m", "S", "M"] = "s", ) -> None: """ Play hangman against the bot, where you have to guess the word it has provided! @@ -71,12 +69,7 @@ class Hangman(commands.Cog): - max_length: the maximum length you want the word to be (i.e. 5) - min_unique_letters: the minimum unique letters you want the word to have (i.e. 4) - max_unique_letters: the maximum unique letters you want the word to have (i.e. 7) - - mode: writing 's' means you want to play by yourself, and only you can suggest letters, - - writing 'm' means you want multiple players to join in and guess the word. """ - # Changing singleplayer to a boolean - singleplayer = mode.lower() == 's' - # Filtering the list of all words depending on the configuration filtered_words = [ word for word in ALL_WORDS @@ -103,11 +96,7 @@ class Hangman(commands.Cog): # Checking if the game is singleplayer def check(msg: Message) -> bool: - if singleplayer: - return msg.author == ctx.author - else: - # Multiplayer mode - return not msg.author.bot + return msg.author == ctx.author and msg.channel == ctx.channel original_message = await ctx.send(embed=Embed( title="Hangman", -- cgit v1.2.3 From 1a981e127a5ad87382c846851c0930226405fcda Mon Sep 17 00:00:00 2001 From: Bluenix Date: Sun, 19 Sep 2021 18:46:51 +0200 Subject: Remove false comment --- bot/exts/fun/hangman.py | 1 - 1 file changed, 1 deletion(-) (limited to 'bot/exts/fun/hangman.py') diff --git a/bot/exts/fun/hangman.py b/bot/exts/fun/hangman.py index db95ba5c..a2c8c735 100644 --- a/bot/exts/fun/hangman.py +++ b/bot/exts/fun/hangman.py @@ -94,7 +94,6 @@ class Hangman(commands.Cog): tries = 6 guessed_letters = set() - # Checking if the game is singleplayer def check(msg: Message) -> bool: return msg.author == ctx.author and msg.channel == ctx.channel -- cgit v1.2.3