diff --git a/gwendolyn/cogs/game_cog.py b/gwendolyn/cogs/game_cog.py index c038560..9d8ddfe 100644 --- a/gwendolyn/cogs/game_cog.py +++ b/gwendolyn/cogs/game_cog.py @@ -98,6 +98,23 @@ class HangmanCog(commands.Cog): """Start a game of hangman.""" await self.bot.games.hangman.start(ctx) +class WordleCog(commands.Cog): + """Contains all the wordle commands.""" + + def __init__(self, bot): + """Initialize the cog.""" + self.bot = bot + + @cog_ext.cog_subcommand(**params["wordle_start"]) + async def wordle_start(self, ctx, letters = 5, hard = False): + """Start a game of wordle.""" + await self.bot.games.wordle.start(ctx, letters, hard) + + @cog_ext.cog_subcommand(**params["wordle_guess"]) + async def wordle_guess(self, ctx, guess): + """Start a game of wordle.""" + await self.bot.games.wordle.guess(ctx, guess) + class HexCog(commands.Cog): """Contains all the hex commands.""" @@ -145,3 +162,4 @@ def setup(bot): bot.add_cog(ConnectFourCog(bot)) bot.add_cog(HangmanCog(bot)) bot.add_cog(HexCog(bot)) + bot.add_cog(WordleCog(bot)) diff --git a/gwendolyn/cogs/misc_cog.py b/gwendolyn/cogs/misc_cog.py index 379eba0..1b76764 100644 --- a/gwendolyn/cogs/misc_cog.py +++ b/gwendolyn/cogs/misc_cog.py @@ -72,7 +72,7 @@ class MiscCog(commands.Cog): @cog_ext.cog_slash(**params["wiki"]) async def wiki(self, ctx: SlashContext, wiki_page=""): """Get a page on a fandom wiki.""" - await self.bot.other.findWikiPage(ctx, page) + await self.bot.other.findWikiPage(ctx, wiki_page) @cog_ext.cog_slash(**params["add_movie"]) async def add_movie(self, ctx: SlashContext, movie): diff --git a/gwendolyn/funcs/games/blackjack.py b/gwendolyn/funcs/games/blackjack.py index 1a22ce9..8b9a448 100644 --- a/gwendolyn/funcs/games/blackjack.py +++ b/gwendolyn/funcs/games/blackjack.py @@ -758,7 +758,7 @@ class Blackjack(CardGame): game_id = datetime.datetime.now().strftime('%Y%m%d%H%M%S') new_game = { - "_id": channel, "dealer hand": dealer_hand, + "dealer hand": dealer_hand, "dealer busted": False, "game_id": game_id, "dealer blackjack": (self._calc_hand_value(dealer_hand) == 21), "user hands": {}, "all standing": False, "round": 0 diff --git a/gwendolyn/funcs/games/game_base.py b/gwendolyn/funcs/games/game_base.py index 6dd6168..66893bc 100644 --- a/gwendolyn/funcs/games/game_base.py +++ b/gwendolyn/funcs/games/game_base.py @@ -105,9 +105,11 @@ class DatabaseGame(GameBase): file_pointer.write(str(old_image.id)) async def _start_new(self, channel: Messageable, new_game: dict, - buttons: list[tuple[str, list]] = None): + buttons: list[tuple[str, list]] = None, + delete = True): + new_game['_id'] = str(channel.id) self._insert_document(new_game) - await self._send_image(channel, buttons) + await self._send_image(channel, buttons, delete) async def _end_game(self, channel: Messageable): await self._delete_old_image(channel) @@ -200,14 +202,22 @@ class BaseDrawer(): self.resources = game.resources game_name = game.game_name self.default_image = f"{self.resources}default_images/{game_name}.png" + self.default_size = None + self.default_color = None self.images_path = f"{self.resources}images/{game_name}" - def _draw_image(self, game: dict, table: Image.Image): + def _get_size(self, game: dict): + return self.default_size + + def _draw_image(self, game: dict, image: Image.Image): pass def draw(self, channel: str): game = self.game.access_document(channel) - image = Image.open(self.default_image) + if self.default_image is not None: + image = Image.open(self.default_image) + else: + image = Image.new("RGB", self._get_size(game), self.default_color) self._draw_image(game, image) self._save_image(image, channel) diff --git a/gwendolyn/funcs/games/games_container.py b/gwendolyn/funcs/games/games_container.py index 7c8a5b2..e653046 100644 --- a/gwendolyn/funcs/games/games_container.py +++ b/gwendolyn/funcs/games/games_container.py @@ -14,6 +14,7 @@ from .blackjack import Blackjack from .connect_four import ConnectFour from .hangman import Hangman from .hex import HexGame +from .wordle import WordleGame class Games(): @@ -46,3 +47,4 @@ class Games(): self.connect_four = ConnectFour(bot) self.hangman = Hangman(bot) self.hex = HexGame(bot) + self.wordle = WordleGame(bot) diff --git a/gwendolyn/funcs/games/wordle.py b/gwendolyn/funcs/games/wordle.py new file mode 100644 index 0000000..c2fc630 --- /dev/null +++ b/gwendolyn/funcs/games/wordle.py @@ -0,0 +1,171 @@ +"""Implementation of Wordle""" +import requests + +from PIL import Image, ImageDraw + +from .game_base import DatabaseGame, BaseDrawer + +IMAGE_MARGIN = 90 +SQUARE_SIZE = 150 +SQUARE_PADDING = 25 +ROW_PADDING = 30 +FONT_SIZE = 130 + +# COLORS +BACKGROUND_COLOR = "#2c2f33" +TEXT_COLOR = "#eee" +WRITING_BORDER_COLOR = "#999" +INACTIVE_BORDER_COLOR = "#555" +INCORRECT_COLOR = "#444" +CORRECT_COLOR = "#0a8c20" +PRESENT_COLOR = "#0f7aa8" + +COLORS = [INCORRECT_COLOR, PRESENT_COLOR, CORRECT_COLOR] + +class WordleGame(DatabaseGame): + """Implementation of wordle game.""" + def __init__(self, bot): + super().__init__(bot, "wordle", WordleDrawer) + self._API_url = "https://api.wordnik.com/v4/words.json/randomWords?" # pylint: disable=invalid-name + api_key = self.bot.credentials["wordnik_key"] + self._APIPARAMS = { # pylint: disable=invalid-name + "hasDictionaryDef": True, + "minCorpusCount": 5000, + "maxCorpusCount": -1, + "minDictionaryCount": 1, + "maxDictionaryCount": -1, + "limit": 1, + "api_key": api_key + } + + async def start(self, ctx, letters: int, hard: bool): + params = self._APIPARAMS + params["minLength"] = letters + params["maxLength"] = letters + word = "-" + while "-" in word or "." in word: + response = requests.get(self._API_url, params=params) + word = list(response.json()[0]["word"].upper()) + + self.bot.log(f"Found the word \"{''.join(word)}\"") + game = {"word": word, "guesses": [], "results": []} + await ctx.send("Starting a wordle game.") + await self._start_new(ctx.channel, game, delete=False) + + def _get_result(self, guess: list[str], word: list[str]): + result = ["_" for _ in guess] + for i, letter in enumerate(guess): + if letter == word[i]: + result[i] = "*" + + for i, letter in enumerate(guess): + if letter in word and result[i] != "*": + if guess[:i].count(letter) < word.count(letter): + result[i] = "-" + + return result + + async def guess(self, ctx, guess: str): + if not guess.isalpha(): + await ctx.send("You can only use letters in your guess") + return + + guess = list(guess.upper()) + game = self.access_document(str(ctx.channel_id)) + + if len(guess) != len(game['word']): + await ctx.send( + f"Your guess must be {len(game['word'])} letters long") + + await ctx.send(f"Guessed {''.join(guess)}") + result = self._get_result(guess, game['word']) + + updater = { + "$set": { + f"guesses.{len(game['guesses'])}": guess, + f"results.{len(game['guesses'])}": result + } + } + self._update_document(str(ctx.channel_id), updater) + + if result == ["*" for _ in game['word']] or len(game['guesses']) == 6: + await self._end_game(ctx.channel) + else: + await self._delete_old_image(ctx.channel) + await self._send_image(ctx.channel) + +class WordleDrawer(BaseDrawer): + def __init__(self, bot, game: WordleGame): + super().__init__(bot, game) + self.default_image = None + self.default_color = BACKGROUND_COLOR + self.default_size = (0, 0) + + def _get_size(self, game: dict): + width = ( + (len(game['word']) * SQUARE_SIZE) + + ((len(game['word']) - 1) * SQUARE_PADDING) + + (2 * IMAGE_MARGIN) + ) + height = ( + (6 * SQUARE_SIZE) + + (5 * ROW_PADDING) + + (2 * IMAGE_MARGIN) + ) + size = (width, height) + + return size + + def _draw_row(self, row, drawer, font, word: str, colors: list[str] = None, + border_color: str = None): + if colors is None: + colors = [BACKGROUND_COLOR for _ in range(len(word))] + + for i, letter in enumerate(word): + y_pos = IMAGE_MARGIN + (row * (SQUARE_SIZE + ROW_PADDING)) + x_pos = IMAGE_MARGIN + (i * (SQUARE_SIZE + SQUARE_PADDING)) + top_left = (x_pos, y_pos) + bottom_right = (x_pos + SQUARE_SIZE, y_pos + SQUARE_SIZE) + + drawer.rounded_rectangle( + (top_left,bottom_right), + 10, + colors[i], + border_color, + 3 + ) + + text_pos = ( + x_pos + (SQUARE_SIZE//2), + y_pos + int(SQUARE_SIZE * 0.6) + ) + + drawer.text(text_pos, letter, TEXT_COLOR, font=font, anchor="mm") + + def _determine_colors(self, results): + return [COLORS[["_","-","*"].index(symbol)] for symbol in results] + + def _draw_image(self, game: dict, image: Image.Image): + drawer = ImageDraw.Draw(image) + font = self._get_font(FONT_SIZE) + for i, guess in enumerate(game['guesses']): + colors = self._determine_colors(game['results'][i]) + self._draw_row(i, drawer, font, guess, colors) + + self._draw_row( + len(game['guesses']), + drawer, + font, + " "*len(game['word']), + border_color=WRITING_BORDER_COLOR + ) + + for i in range(5 - len(game['guesses'])): + self._draw_row( + len(game['guesses']) + i + 1, + drawer, + font, " "*len(game['word']), + border_color=INACTIVE_BORDER_COLOR + ) + + return super()._draw_image(game, image) diff --git a/gwendolyn/resources/slash_parameters.json b/gwendolyn/resources/slash_parameters.json index 532d552..aa46666 100644 --- a/gwendolyn/resources/slash_parameters.json +++ b/gwendolyn/resources/slash_parameters.json @@ -383,5 +383,37 @@ "required" : "true" } ] + }, + "wordle_start": { + "base": "wordle", + "name" : "start", + "description": "Start a game of wordle", + "options": [ + { + "name": "letters", + "description" : "How many letters the word should be", + "type": 4, + "required": "false" + }, + { + "name": "hard", + "description" : "Whether the game should be in 'hard mode'", + "type": 5, + "required": "false" + } + ] + }, + "wordle_guess": { + "base": "wordle", + "name" : "guess", + "description": "Guess a word in wordle", + "options": [ + { + "name": "guess", + "description" : "Your guess", + "type": 3, + "required": "true" + } + ] } } \ No newline at end of file diff --git a/gwendolyn/utils/helper_classes.py b/gwendolyn/utils/helper_classes.py index ca20c5d..cb13179 100644 --- a/gwendolyn/utils/helper_classes.py +++ b/gwendolyn/utils/helper_classes.py @@ -107,7 +107,8 @@ class DatabaseFuncs(): "trivia questions", "blackjack games", "connect 4 games" - "hex games" + "hex games", + "wordle games" ] for game_type in game_types: self.bot.database[game_type].delete_many({})