aboutsummaryrefslogtreecommitdiffstats
path: root/bot/exts/fun/hangman.py
diff options
context:
space:
mode:
authorGravatar Shom770 <[email protected]>2021-09-19 00:45:36 -0400
committerGravatar GitHub <[email protected]>2021-09-18 21:45:36 -0700
commit874326c4bb05ad36224e388d134691c55cd2141a (patch)
tree6b2320ffbfb328a205dd142c465849eea1bf7090 /bot/exts/fun/hangman.py
parentMerge pull request #864 from NIRDERIi/hanukkah_cmd_month (diff)
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 <[email protected]> * 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 <[email protected]> * Improving tries logic Co-authored-by: Bluenix <[email protected]> * Updating `positions` list to set Co-authored-by: Bluenix <[email protected]> * fixing too many blank lines * Hardcode dictionary Co-authored-by: Bluenix <[email protected]> * 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 <[email protected]> * Grammatical error Co-authored-by: Bluenix <[email protected]> * Simplification Co-authored-by: ChrisJL <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * Renaming parameter from `singleplayer` to `mode` Co-authored-by: Bluenix <[email protected]> Co-authored-by: ChrisJL <[email protected]> Co-authored-by: Bluenix <[email protected]> Co-authored-by: DMFriends <[email protected]>
Diffstat (limited to 'bot/exts/fun/hangman.py')
-rw-r--r--bot/exts/fun/hangman.py194
1 files changed, 194 insertions, 0 deletions
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))