aboutsummaryrefslogtreecommitdiffstats
path: root/bot/exts/fun/hangman.py
diff options
context:
space:
mode:
authorGravatar Numerlor <[email protected]>2021-11-09 22:48:30 +0100
committerGravatar Numerlor <[email protected]>2021-11-09 22:48:30 +0100
commitd4e47c80aedfdd82a6aa4ce81a8ade4219819d0c (patch)
tree6a1595cd648c52232593a82a84731de6c027a955 /bot/exts/fun/hangman.py
parentAdd selective trace loggers (diff)
parentMerge pull request #915 from python-discord/Add-border-around-bat (diff)
Merge remote-tracking branch 'upstream/main' into coloredlogs
Diffstat (limited to 'bot/exts/fun/hangman.py')
-rw-r--r--bot/exts/fun/hangman.py182
1 files changed, 182 insertions, 0 deletions
diff --git a/bot/exts/fun/hangman.py b/bot/exts/fun/hangman.py
new file mode 100644
index 00000000..a2c8c735
--- /dev/null
+++ b/bot/exts/fun/hangman.py
@@ -0,0 +1,182 @@
+from asyncio import TimeoutError
+from pathlib import Path
+from random import choice
+
+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,
+ ) -> 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)
+ """
+ # 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()
+
+ def check(msg: Message) -> bool:
+ return msg.author == ctx.author and msg.channel == ctx.channel
+
+ 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))