diff --git a/gwendolyn/cogs/game_cog.py b/gwendolyn/cogs/game_cog.py index 8b04fd9..76a9d9d 100644 --- a/gwendolyn/cogs/game_cog.py +++ b/gwendolyn/cogs/game_cog.py @@ -118,16 +118,11 @@ class HangmanCog(commands.Cog): """Initialize the cog.""" self.bot = bot - @cog_ext.cog_subcommand(**params["hangman_start"]) - async def hangman_start(self, ctx): + @cog_ext.cog_slash(**params["hangman"]) + async def hangman(self, ctx): """Start a game of hangman.""" await self.bot.games.hangman.start(ctx) - @cog_ext.cog_subcommand(**params["hangman_stop"]) - async def hangman_stop(self, ctx): - """Stop the current game of hangman.""" - await self.bot.games.hangman.stop(ctx) - class HexCog(commands.Cog): """Contains all the hex commands.""" diff --git a/gwendolyn/funcs/games/hangman.py b/gwendolyn/funcs/games/hangman.py index 0f247e4..b4373f7 100644 --- a/gwendolyn/funcs/games/hangman.py +++ b/gwendolyn/funcs/games/hangman.py @@ -14,10 +14,16 @@ import math # Used by DrawHangman(), mainly for drawing circles import random # Used to draw poorly import requests # Used for getting the word in Hangman.start() import discord # Used for discord.file and type hints +import os -from discord_slash.context import SlashContext # Used for typehints +from discord_slash.utils.manage_components import (create_button, +create_actionrow) +from discord_slash.model import ButtonStyle +from discord_slash.context import SlashContext, ComponentContext +# Used for typehints from PIL import ImageDraw, Image, ImageFont # Used to draw the image +from gwendolyn.utils import encode_id class Hangman(): """ @@ -43,10 +49,10 @@ class Hangman(): APIPARAMS: dict The parameters to pass to every api call. """ - self.__bot = bot + self.bot = bot self.__draw = DrawHangman(bot) self.__API_url = "https://api.wordnik.com/v4/words.json/randomWords?" # pylint: disable=invalid-name - api_key = self.__bot.credentials["wordnik_key"] + api_key = self.bot.credentials["wordnik_key"] self.__APIPARAMS = { # pylint: disable=invalid-name "hasDictionaryDef": True, "minCorpusCount": 5000, @@ -68,70 +74,88 @@ class Hangman(): ctx: SlashContext The context of the command. """ - await self.__bot.defer(ctx) - channel = str(ctx.channel_id) - user = f"#{ctx.author.id}" - game = self.__bot.database["hangman games"].find_one({"_id": channel}) - user_name = self.__bot.database_funcs.get_name(user) - started_game = False + await self.bot.defer(ctx) - if game is None: - word = "-" - while "-" in word or "." in word: - response = requests.get(self.__API_url, params=self.__APIPARAMS) - word = list(response.json()[0]["word"].upper()) + word = "-" + while "-" in word or "." in word: + response = requests.get(self.__API_url, params=self.__APIPARAMS) + word = list(response.json()[0]["word"].upper()) - self.__bot.log("Found the word \""+"".join(word)+"\"") - guessed = [False] * len(word) - game_id = datetime.datetime.now().strftime("%Y%m%d%H%M%S") - new_game = { - "_id": channel, - "player": user, - "guessed letters": [], - "word": word, - "game ID": game_id, - "misses": 0, - "guessed": guessed - } - self.__bot.database["hangman games"].insert_one(new_game) + self.bot.log("Found the word \""+"".join(word)+"\"") + guessed = [False] * len(word) + game_id = datetime.datetime.now().strftime("%Y%m%d%H%M%S") - remaining_letters = list(string.ascii_uppercase) + remaining_letters = list(string.ascii_uppercase) - self.__draw.draw_image(channel) + self.__draw.draw_image(game_id, 0, word, guessed, []) - log_message = "Game started" - send_message = f"{user_name} started game of hangman." - started_game = True - else: - log_message = "There was already a game going on" - send_message = self.__bot.long_strings["Hangman going on"] + send_message = f"{ctx.author.display_name} started a game of hangman." - self.__bot.log(log_message) - await ctx.send(send_message) + self.bot.log("Game started") - if started_game: - boards_path = "gwendolyn/resources/games/hangman_boards/" - file_path = f"{boards_path}hangman_board{channel}.png" - new_image = await ctx.channel.send(file=discord.File(file_path)) + boards_path = "gwendolyn/resources/games/hangman_boards/" + file_path = f"{boards_path}hangman_board{game_id}.png" + image_message = await ctx.send( + send_message, + file=discord.File(file_path) + ) - blank_message = await ctx.channel.send("_ _") - reaction_messages = { - new_image: remaining_letters[:15], - blank_message: remaining_letters[15:] - } + blank_message_one = await ctx.channel.send("_ _") + blank_message_two = await ctx.channel.send("_ _") - old_messages = f"{new_image.id}\n{blank_message.id}" - old_images_path = "gwendolyn/resources/games/old_images/" + buttons = [] + for letter in remaining_letters: + custom_id = encode_id([ + "hangman", + "guess", + str(ctx.author.id), + letter, + "".join(word), + "", + game_id, + str(image_message.id), + str(blank_message_one.id), + str(blank_message_two.id) + ]) + buttons.append(create_button( + style=ButtonStyle.blue, + label=letter, + custom_id=custom_id + )) - with open(old_images_path+f"hangman{channel}", "w") as file_pointer: - file_pointer.write(old_messages) + custom_id = encode_id([ + "hangman", + "end", + str(ctx.author.id), + game_id, + str(image_message.id), + str(blank_message_one.id), + str(blank_message_two.id) + ]) + buttons.append(create_button( + style=ButtonStyle.red, + label="End game", + custom_id=custom_id + )) - for message, letters in reaction_messages.items(): - for letter in letters: - emoji = chr(ord(letter)+127397) - await message.add_reaction(emoji) + action_row_lists = [] + for i in range(((len(buttons)-1)//20)+1): + action_rows = [] + available_buttons = buttons[(i*20):(min(len(buttons),i*20+20))] + for x in range(((len(available_buttons)-1)//4)+1): + row_buttons = available_buttons[ + (x*4):(min(len(available_buttons),x*4+4)) + ] + action_rows.append(create_actionrow(*row_buttons)) + action_row_lists.append(action_rows) - async def stop(self, ctx: SlashContext): + + await blank_message_one.edit(components=action_row_lists[0]) + await blank_message_two.edit(components=action_row_lists[1]) + + + async def stop(self, ctx: ComponentContext, game_id: str, + *messages: list[str]): """ Stop the game of hangman. @@ -140,28 +164,19 @@ class Hangman(): ctx: SlashContext The context of the command. """ - channel = str(ctx.channel.id) - game = self.__bot.database["hangman games"].find_one({"_id": channel}) + for msg_id in messages: + msg = await ctx.channel.fetch_message(msg_id) + await msg.delete() - if game is None: - await ctx.send("There's no game going on") - elif f"#{ctx.author.id}" != game["player"]: - await ctx.send("You can't end a game you're not in") - else: - self.__bot.database["hangman games"].delete_one({"_id": channel}) - old_images_path = "gwendolyn/resources/games/old_images/" + self.bot.log("Deleting old messages") - with open(old_images_path+f"hangman{channel}", "r") as file_pointer: - messages = file_pointer.read().splitlines() + await ctx.channel.send("Hangman game stopped") + boards_path = "gwendolyn/resources/games/hangman_boards/" + file_path = f"{boards_path}hangman_board{game_id}.png" + os.remove(file_path) - for message in messages: - old_message = await ctx.channel.fetch_message(int(message)) - self.__bot.log("Deleting old message") - await old_message.delete() - - await ctx.send("Game stopped") - - async def guess(self, message: discord.Message, user: str, guess: str): + async def guess(self, ctx: ComponentContext, guess: str, word: str, + guessed_letters: str, game_id: str, *messages: list[str]): """ Guess a letter. @@ -174,98 +189,112 @@ class Hangman(): guess: str The guess. """ - channel = str(message.channel.id) - hangman_games = self.__bot.database["hangman games"] - game = hangman_games.find_one({"_id": channel}) + for msg_id in messages: + msg = await ctx.channel.fetch_message(msg_id) + await msg.delete() - game_exists = (game is not None) - single_letter = (len(guess) == 1 and guess.isalpha()) - new_guess = (guess not in game["guessed letters"]) - valid_guess = (game_exists and single_letter and new_guess) - - if valid_guess: - self.__bot.log("Guessed the letter") + misses = 0 + guessed_letters += guess + guessed = [False for i in range(len(word))] + for guessed_letter in guessed_letters: correct_guess = 0 - - for i, letter in enumerate(game["word"]): - if guess == letter: + for i, letter in enumerate(word): + if guessed_letter == letter: correct_guess += 1 - updater = {"$set": {f"guessed.{i}": True}} - hangman_games.update_one({"_id": channel}, updater) + guessed[i] = True if correct_guess == 0: - updater = {"$inc": {"misses": 1}} - hangman_games.update_one({"_id": channel}, updater) + misses += 1 - updater = {"$push": {"guessed letters": guess}} - hangman_games.update_one({"_id": channel}, updater) - remaining_letters = list(string.ascii_uppercase) + remaining_letters = list(string.ascii_uppercase) + for letter in guessed_letters: + remaining_letters.remove(letter) - game = hangman_games.find_one({"_id": channel}) + if correct_guess == 1: + send_message = "Guessed {}. There was 1 {} in the word." + send_message = send_message.format(guess, guess) + else: + send_message = "Guessed {}. There were {} {}s in the word." + send_message = send_message.format(guess, correct_guess, guess) - for letter in game["guessed letters"]: - remaining_letters.remove(letter) + self.__draw.draw_image(game_id, misses, word, guessed, guessed_letters) - if correct_guess == 1: - send_message = "Guessed {}. There was 1 {} in the word." - send_message = send_message.format(guess, guess) - else: - send_message = "Guessed {}. There were {} {}s in the word." - send_message = send_message.format(guess, correct_guess, guess) + if misses == 6: + send_message += self.bot.long_strings["Hangman lost game"] + remaining_letters = [] + elif all(guessed): + self.bot.money.addMoney(f"#{ctx.author_id}", 15) + send_message += self.bot.long_strings["Hangman guessed word"] + remaining_letters = [] - self.__draw.draw_image(channel) - - if game["misses"] == 6: - hangman_games.delete_one({"_id": channel}) - send_message += self.__bot.long_strings["Hangman lost game"] - remaining_letters = [] - elif all(game["guessed"]): - hangman_games.delete_one({"_id": channel}) - self.__bot.money.addMoney(user, 15) - send_message += self.__bot.long_strings["Hangman guessed word"] - remaining_letters = [] - - await message.channel.send(send_message) - old_images_path = "gwendolyn/resources/games/old_images/" - - with open(old_images_path+f"hangman{channel}", "r") as file_pointer: - old_message_ids = file_pointer.read().splitlines() - - for old_id in old_message_ids: - old_message = await message.channel.fetch_message(int(old_id)) - self.__bot.log("Deleting old message") - await old_message.delete() + if remaining_letters != []: boards_path = "gwendolyn/resources/games/hangman_boards/" - file_path = f"{boards_path}hangman_board{channel}.png" - new_image = await message.channel.send(file=discord.File(file_path)) + file_path = f"{boards_path}hangman_board{game_id}.png" + image_message = await ctx.channel.send( + send_message, + file=discord.File(file_path) + ) + blank_message_one = await ctx.channel.send("_ _") + blank_message_two = await ctx.channel.send("_ _") + buttons = [] + for letter in list(string.ascii_uppercase): + custom_id = encode_id([ + "hangman", + "guess", + str(ctx.author.id), + letter, + word, + guessed_letters, + game_id, + str(image_message.id), + str(blank_message_one.id), + str(blank_message_two.id) + ]) + buttons.append(create_button( + style=ButtonStyle.blue, + label=letter, + custom_id=custom_id, + disabled=(letter not in remaining_letters) + )) + custom_id = encode_id([ + "hangman", + "end", + str(ctx.author.id), + game_id, + str(image_message.id), + str(blank_message_one.id), + str(blank_message_two.id) + ]) + buttons.append(create_button( + style=ButtonStyle.red, + label="End game", + custom_id=custom_id + )) - if len(remaining_letters) > 0: - if len(remaining_letters) > 15: - blank_message = await message.channel.send("_ _") - reaction_messages = { - new_image: remaining_letters[:15], - blank_message: remaining_letters[15:] - } - else: - blank_message = "" - reaction_messages = {new_image: remaining_letters} + action_row_lists = [] + for i in range(((len(buttons)-1)//20)+1): + action_rows = [] + available_buttons = buttons[(i*20):(min(len(buttons),i*20+20))] + for x in range(((len(available_buttons)-1)//4)+1): + row_buttons = available_buttons[ + (x*4):(min(len(available_buttons),x*4+4)) + ] + action_rows.append(create_actionrow(*row_buttons)) + action_row_lists.append(action_rows) + + await blank_message_one.edit(components=action_row_lists[0]) + await blank_message_two.edit(components=action_row_lists[1]) + else: + boards_path = "gwendolyn/resources/games/hangman_boards/" + file_path = f"{boards_path}hangman_board{game_id}.png" + await ctx.channel.send(send_message, file=discord.File(file_path)) + + os.remove(file_path) - if blank_message != "": - old_messages = f"{new_image.id}\n{blank_message.id}" - else: - old_messages = str(new_image.id) - old_images_path = "gwendolyn/resources/games/old_images/" - old_image_path = old_images_path + f"hangman{channel}" - with open(old_image_path, "w") as file_pointer: - file_pointer.write(old_messages) - for message, letters in reaction_messages.items(): - for letter in letters: - emoji = chr(ord(letter)+127397) - await message.add_reaction(emoji) class DrawHangman(): @@ -355,9 +384,9 @@ class DrawHangman(): deviance_accuracy_x = 0 deviance_accuracy_y = 0 start = random.randint(-100, -80) - degreess_amount = 360 + random.randint(-10, 30) + degrees_amount = 360 + random.randint(-10, 30) - for degree in range(degreess_amount): + for degree in range(degrees_amount): deviance_x, deviance_accuracy_x = self.__deviate( deviance_x, deviance_accuracy_x, @@ -589,7 +618,8 @@ class DrawHangman(): placed = True return background - def draw_image(self, channel: str): + def draw_image(self, game_id: str, misses: int, word: str, + guessed: list[bool], guessed_letters: str): """ Draw a hangman Image. @@ -598,21 +628,20 @@ class DrawHangman(): channel: str The id of the channel the game is in. """ - self.__bot.log("Drawing hangman image", channel) - game = self.__bot.database["hangman games"].find_one({"_id": channel}) + self.__bot.log("Drawing hangman image") - random.seed(game["game ID"]) + random.seed(game_id) background = Image.open("gwendolyn/resources/paper.jpg") gallow = self.__draw_gallows() - man = self.__draw_man(game["misses"], game["game ID"]) + man = self.__draw_man(misses, game_id) - random.seed(game["game ID"]) - letter_line_parameters = [game["word"], game["guessed"], game["misses"]] + random.seed(game_id) + letter_line_parameters = [word, guessed, misses] letter_lines = self.__draw_letter_lines(*letter_line_parameters) - random.seed(game["game ID"]) - misses = self.__draw_misses(game["guessed letters"], game["word"]) + random.seed(game_id) + misses = self.__draw_misses(list(guessed_letters), word) background.paste(gallow, (100, 100), gallow) background.paste(man, (300, 210), man) @@ -623,5 +652,6 @@ class DrawHangman(): misses_text_width = misses_text.size[0] background.paste(misses_text, (850-misses_text_width//2, 50), misses_text) - board_path = f"gwendolyn/resources/games/hangman_boards/hangman_board{channel}.png" - background.save(board_path) + boards_path = "gwendolyn/resources/games/hangman_boards/" + image_path = f"{boards_path}hangman_board{game_id}.png" + background.save(image_path) diff --git a/gwendolyn/funcs/other/plex.py b/gwendolyn/funcs/other/plex.py index a30f878..d538aac 100644 --- a/gwendolyn/funcs/other/plex.py +++ b/gwendolyn/funcs/other/plex.py @@ -11,6 +11,8 @@ from discord_slash.utils.manage_components import (create_button, create_actionrow) from discord_slash.model import ButtonStyle +from gwendolyn.utils import encode_id + class Plex(): """Container for Plex functions and commands.""" def __init__(self,bot): @@ -73,7 +75,7 @@ class Plex(): buttons.append(create_button( style=ButtonStyle.green, label="✓", - custom_id=f"plex:movie:{imdb_ids[0]}" + custom_id=encode_id(["plex", "movie", str(imdb_ids[0])]) ) ) else: @@ -82,14 +84,14 @@ class Plex(): create_button( style=ButtonStyle.blue, label=str(i+1), - custom_id=f"plex:movie:{imdb_ids[i]}" + custom_id=encode_id(["plex", "movie", str(imdb_ids[i])]) ) ) buttons.append(create_button( style=ButtonStyle.red, label="X", - custom_id="plex:movie:" + custom_id=encode_id(["plex", "movie", ""]) ) ) @@ -211,7 +213,7 @@ class Plex(): buttons.append(create_button( style=ButtonStyle.green, label="✓", - custom_id=f"plex:show:{imdb_ids[0]}" + custom_id=encode_id(["plex", "show", str(imdb_ids[0])]) ) ) else: @@ -220,14 +222,14 @@ class Plex(): create_button( style=ButtonStyle.blue, label=str(i+1), - custom_id=f"plex:show:{imdb_ids[i]}" + custom_id=encode_id(["plex", "show", str(imdb_ids[i])]) ) ) buttons.append(create_button( style=ButtonStyle.red, label="X", - custom_id="plex:show:" + custom_id=encode_id(["plex", "show", ""]) ) ) diff --git a/gwendolyn/resources/slash_parameters.json b/gwendolyn/resources/slash_parameters.json index 275b2e9..479e254 100644 --- a/gwendolyn/resources/slash_parameters.json +++ b/gwendolyn/resources/slash_parameters.json @@ -187,16 +187,10 @@ } ] }, - "hangman_start" : { - "base" : "hangman", - "name" : "start", + "hangman" : { + "name" : "hangman", "description" : "Start a game of hangman" }, - "hangman_stop" : { - "base" : "hangman", - "name" : "stop", - "description" : "Stop the current game of hangman" - }, "hello" : { "name" : "hello", "description" : "Greet Gwendolyn" diff --git a/gwendolyn/utils/__init__.py b/gwendolyn/utils/__init__.py index 863b018..c8353c0 100644 --- a/gwendolyn/utils/__init__.py +++ b/gwendolyn/utils/__init__.py @@ -8,4 +8,5 @@ from .helper_classes import DatabaseFuncs from .event_handlers import EventHandler, ErrorHandler from .util_functions import (get_params, log_this, cap, make_files, replace_multiple, emoji_to_command, long_strings, - sanitize, get_options, get_credentials) + sanitize, get_options, get_credentials, encode_id, + decode_id) diff --git a/gwendolyn/utils/event_handlers.py b/gwendolyn/utils/event_handlers.py index 57579d6..3c297b2 100644 --- a/gwendolyn/utils/event_handlers.py +++ b/gwendolyn/utils/event_handlers.py @@ -16,7 +16,7 @@ from discord.ext import commands # Used to compare errors with command # errors from discord_slash.context import SlashContext, ComponentContext -from gwendolyn.utils.util_functions import emoji_to_command +from gwendolyn.utils.util_functions import emoji_to_command, decode_id class EventHandler(): @@ -79,37 +79,32 @@ class EventHandler(): params = [message, f"#{user.id}", column-1] await self.bot.games.connect_four.place_piece(*params) - - elif tests.hangman_reaction_test(*reaction_test_parameters): - self.bot.log("They reacted to the hangman message") - if ord(reaction.emoji) in range(127462, 127488): - # The range is letter-emojis - guess = chr(ord(reaction.emoji)-127397) - # Converts emoji to letter - params = [message, f"#{user.id}", guess] - await self.bot.games.hangman.guess(*params) - else: - self.bot.log("Bot they didn't react with a valid guess") - async def on_component(self, ctx: ComponentContext): - info = ctx.custom_id.split(":") + info = decode_id(ctx.custom_id) channel = ctx.origin_message.channel - if info[0] == "plex": - if info[1] == "movie": + if info[0].lower() == "plex": + if info[1].lower() == "movie": await self.bot.other.plex.add_movie( ctx.origin_message, info[2], not isinstance(channel, discord.DMChannel) ) - if info[1] == "show": + elif info[1].lower() == "show": await self.bot.other.plex.add_show( ctx.origin_message, info[2], not isinstance(channel, discord.DMChannel) ) + elif info[0].lower() == "hangman": + if str(ctx.author_id) == info[2]: + if info[1].lower() == "guess": + await self.bot.games.hangman.guess(ctx, *info[3:]) + elif info[1].lower() == "end": + await self.bot.games.hangman.stop(ctx, *info[3:]) + class ErrorHandler(): """ diff --git a/gwendolyn/utils/helper_classes.py b/gwendolyn/utils/helper_classes.py index 4735987..4e9333e 100644 --- a/gwendolyn/utils/helper_classes.py +++ b/gwendolyn/utils/helper_classes.py @@ -24,11 +24,6 @@ class DatabaseFuncs(): wipe_games() connect_four_reaction_test(message: discord.Message, user: discord.User) -> bool - hangman_reaction_test(message: discord.Message, - user: discord.User) -> bool - plex_reaction_test(message: discord.Message, - user: discord.User) -> bool, bool, - list imdb_commands() """ @@ -111,8 +106,7 @@ class DatabaseFuncs(): game_types = [ "trivia questions", "blackjack games", - "connect 4 games", - "hangman games", + "connect 4 games" "hex games" ] for game_type in game_types: @@ -163,48 +157,6 @@ class DatabaseFuncs(): return valid_reaction - def hangman_reaction_test(self, message: discord.Message, - user: discord.User): - """ - Test if the given message is the current hangman game. - - Also tests if the given user is the one who's playing hangman. - - *Parameters* - ------------ - message: discord.Message - The message to test. - user: discord.User - The user to test. - *Returns* - --------- - : bool - Whether the given message is the current hangman game - and if the user who reacted is the user who's playing - hangman. - """ - channel = message.channel - file_path = f"gwendolyn/resources/games/old_images/hangman{channel.id}" - if os.path.isfile(file_path): - with open(file_path, "r") as file_pointer: - old_messages = file_pointer.read().splitlines() - else: - return False - game_message = False - - for old_message in old_messages: - old_message_id = int(old_message) - if message.id == old_message_id: - database = self.bot.database["hangman games"] - channel_search = {"_id": str(channel.id)} - game = database.find_one(channel_search) - if user == game["player"]: - game_message = True - - break - - return game_message - async def imdb_commands(self): """Sync the slash commands with the discord API.""" collection = self.bot.database["last synced"] diff --git a/gwendolyn/utils/util_functions.py b/gwendolyn/utils/util_functions.py index 167e309..853a4c9 100644 --- a/gwendolyn/utils/util_functions.py +++ b/gwendolyn/utils/util_functions.py @@ -21,6 +21,17 @@ import logging # Used for logging import os # Used by make_files() to check if files exist import sys # Used to specify printing for logging import imdb # Used to disable logging for the module +import string + + +BASE_37 = ":" + string.digits + string.ascii_uppercase +BASE_128 = list( + string.digits + + string.ascii_letters + + "!#$€£¢¥¤&%()*+,-./;:<=>?@[]_{|}~ `¦§©®«»±µ·¿əʒ" + + "ÆØÅÐÉÈÊÇÑÖ" + + "æøåðéèêçñö" +) # All of this is logging configuration FORMAT = " %(asctime)s | %(name)-16s | %(levelname)-8s | %(message)s" @@ -340,3 +351,39 @@ def emoji_to_command(emoji: str): return_value = "" return return_value + +def encode_id(info: list): + letters = list(":".join(info)) + dec = 0 + for i, letter in enumerate(letters): + try: + dec += (37**i) * BASE_37.index(letter.upper()) + except ValueError: + log_this("Could not encode letter {letter}", level=30) + + custom_id = [] + + while dec: + custom_id.append(BASE_128[dec % 128]) + dec = dec // 128 + + custom_id = ''.join(custom_id) + log_this(f"Encoded {info} to {custom_id}") + return custom_id + + +def decode_id(custom_id: str): + letters = list(custom_id) + dec = 0 + for i, letter in enumerate(letters): + dec += (128**i) * BASE_128.index(letter) + + info_string = [] + + while dec: + info_string.append(BASE_37[dec % 37]) + dec = dec // 37 + + info = ''.join(info_string).split(':') + log_this(f"Decoded {custom_id} to be {info}") + return ''.join(info_string).split(':')