diff options
Diffstat (limited to 'bot/exts/fun')
-rw-r--r-- | bot/exts/fun/anagram.py | 110 | ||||
-rw-r--r-- | bot/exts/fun/duck_game.py | 42 | ||||
-rw-r--r-- | bot/exts/fun/tic_tac_toe.py | 16 | ||||
-rw-r--r-- | bot/exts/fun/trivia_quiz.py | 4 |
4 files changed, 148 insertions, 24 deletions
diff --git a/bot/exts/fun/anagram.py b/bot/exts/fun/anagram.py new file mode 100644 index 00000000..9aee5f18 --- /dev/null +++ b/bot/exts/fun/anagram.py @@ -0,0 +1,110 @@ +import asyncio +import json +import logging +import random +from pathlib import Path + +import discord +from discord.ext import commands + +from bot.bot import Bot +from bot.constants import Colours + +log = logging.getLogger(__name__) + +TIME_LIMIT = 60 + +# anagram.json file contains all the anagrams +with open(Path("bot/resources/fun/anagram.json"), "r") as f: + ANAGRAMS_ALL = json.load(f) + + +class AnagramGame: + """ + Used for creating instances of anagram games. + + Once multiple games can be run at the same time, this class' instances + can be used for keeping track of each anagram game. + """ + + def __init__(self, scrambled: str, correct: list[str]) -> None: + self.scrambled = scrambled + self.correct = set(correct) + + self.winners = set() + + async def message_creation(self, message: discord.Message) -> None: + """Check if the message is a correct answer and remove it from the list of answers.""" + if message.content.lower() in self.correct: + self.winners.add(message.author.mention) + self.correct.remove(message.content.lower()) + + +class Anagram(commands.Cog): + """Cog for the Anagram game command.""" + + def __init__(self, bot: Bot): + self.bot = bot + + self.games: dict[int, AnagramGame] = {} + + @commands.command(name="anagram", aliases=("anag", "gram", "ag")) + @commands.guild_only() + async def anagram_command(self, ctx: commands.Context) -> None: + """ + Given shuffled letters, rearrange them into anagrams. + + Show an embed with scrambled letters which if rearranged can form words. + After a specific amount of time, list the correct answers and whether someone provided a + correct answer. + """ + if self.games.get(ctx.channel.id): + await ctx.send("An anagram is already being solved in this channel!") + return + + scrambled_letters, correct = random.choice(list(ANAGRAMS_ALL.items())) + + game = AnagramGame(scrambled_letters, correct) + self.games[ctx.channel.id] = game + + anagram_embed = discord.Embed( + title=f"Find anagrams from these letters: '{scrambled_letters.upper()}'", + description=f"You have {TIME_LIMIT} seconds to find correct words.", + colour=Colours.purple, + ) + + await ctx.send(embed=anagram_embed) + await asyncio.sleep(TIME_LIMIT) + + if game.winners: + win_list = ", ".join(game.winners) + content = f"Well done {win_list} for getting it right!" + else: + content = "Nobody got it right." + + answer_embed = discord.Embed( + title=f"The words were: `{'`, `'.join(ANAGRAMS_ALL[game.scrambled])}`!", + colour=Colours.pink, + ) + + await ctx.send(content, embed=answer_embed) + + # Game is finished, let's remove it from the dict + self.games.pop(ctx.channel.id) + + @commands.Cog.listener() + async def on_message(self, message: discord.Message) -> None: + """Check a message for an anagram attempt and pass to an ongoing game.""" + if message.author.bot or not message.guild: + return + + game = self.games.get(message.channel.id) + if not game: + return + + await game.message_creation(message) + + +def setup(bot: Bot) -> None: + """Load the Anagram cog.""" + bot.add_cog(Anagram(bot)) diff --git a/bot/exts/fun/duck_game.py b/bot/exts/fun/duck_game.py index 1ef7513f..10b03a49 100644 --- a/bot/exts/fun/duck_game.py +++ b/bot/exts/fun/duck_game.py @@ -11,7 +11,7 @@ from PIL import Image, ImageDraw, ImageFont from discord.ext import commands from bot.bot import Bot -from bot.constants import Colours, MODERATION_ROLES +from bot.constants import MODERATION_ROLES from bot.utils.decorators import with_role DECK = list(product(*[(0, 1, 2)]*4)) @@ -130,6 +130,9 @@ class DuckGame: while len(self.solutions) < minimum_solutions: self.board = random.sample(DECK, size) + self.board_msg = None + self.found_msg = None + @property def board(self) -> list[tuple[int]]: """Accesses board property.""" @@ -181,7 +184,7 @@ class DuckGamesDirector(commands.Cog): ) @commands.cooldown(rate=1, per=2, type=commands.BucketType.channel) async def start_game(self, ctx: commands.Context) -> None: - """Generate a board, send the game embed, and end the game after a time limit.""" + """Start a new Duck Duck Duck Goose game.""" if ctx.channel.id in self.current_games: await ctx.send("There's already a game running!") return @@ -191,8 +194,8 @@ class DuckGamesDirector(commands.Cog): game.running = True self.current_games[ctx.channel.id] = game - game.msg_content = "" - game.embed_msg = await self.send_board_embed(ctx, game) + game.board_msg = await self.send_board_embed(ctx, game) + game.found_msg = await self.send_found_embed(ctx) await asyncio.sleep(GAME_DURATION) # Checking for the channel ID in the currently running games is not sufficient. @@ -245,13 +248,13 @@ class DuckGamesDirector(commands.Cog): if answer in game.solutions: game.claimed_answers[answer] = msg.author game.scores[msg.author] += CORRECT_SOLN - await self.display_claimed_answer(game, msg.author, answer) + await self.append_to_found_embed(game, f"{str(answer):12s} - {msg.author.display_name}") else: await msg.add_reaction(EMOJI_WRONG) game.scores[msg.author] += INCORRECT_SOLN async def send_board_embed(self, ctx: commands.Context, game: DuckGame) -> discord.Message: - """Create and send the initial game embed. This will be edited as the game goes on.""" + """Create and send an embed to display the board.""" image = assemble_board_image(game.board, game.rows, game.columns) with BytesIO() as image_stream: image.save(image_stream, format="png") @@ -259,19 +262,27 @@ class DuckGamesDirector(commands.Cog): file = discord.File(fp=image_stream, filename="board.png") embed = discord.Embed( title="Duck Duck Duck Goose!", - color=Colours.bright_green, + color=discord.Color.dark_purple(), ) embed.set_image(url="attachment://board.png") return await ctx.send(embed=embed, file=file) - async def display_claimed_answer(self, game: DuckGame, author: discord.Member, answer: tuple[int]) -> None: - """Add a claimed answer to the game embed.""" + async def send_found_embed(self, ctx: commands.Context) -> discord.Message: + """Create and send an embed to display claimed answers. This will be edited as the game goes on.""" + # Can't be part of the board embed because of discord.py limitations with editing an embed with an image. + embed = discord.Embed( + title="Flights Found", + color=discord.Color.dark_purple(), + ) + return await ctx.send(embed=embed) + + async def append_to_found_embed(self, game: DuckGame, text: str) -> None: + """Append text to the claimed answers embed.""" async with game.editing_embed: - # We specifically edit the message contents instead of the embed - # Because we load in the image from the file, editing any portion of the embed - # Does weird things to the image and this works around that weirdness - game.msg_content = f"{game.msg_content}\n{str(answer):12s} - {author.display_name}" - await game.embed_msg.edit(content=game.msg_content) + found_embed, = game.found_msg.embeds + old_desc = found_embed.description or "" + found_embed.description = f"{old_desc.rstrip()}\n{text}" + await game.found_msg.edit(embed=found_embed) async def end_game(self, channel: discord.TextChannel, game: DuckGame, end_message: str) -> None: """Edit the game embed to reflect the end of the game and mark the game as not running.""" @@ -296,8 +307,7 @@ class DuckGamesDirector(commands.Cog): missed_text = "Flights everyone missed:\n" + "\n".join(f"{ans}" for ans in missed) else: missed_text = "All the flights were found!" - - await game.embed_msg.edit(content=f"{missed_text}") + await self.append_to_found_embed(game, f"\n{missed_text}") @start_game.command(name="help") async def show_rules(self, ctx: commands.Context) -> None: diff --git a/bot/exts/fun/tic_tac_toe.py b/bot/exts/fun/tic_tac_toe.py index 5c4f8051..946b6f7b 100644 --- a/bot/exts/fun/tic_tac_toe.py +++ b/bot/exts/fun/tic_tac_toe.py @@ -72,10 +72,12 @@ class Player: class AI: """Tic Tac Toe AI class for against computer gaming.""" - def __init__(self, symbol: str): + def __init__(self, bot_user: discord.Member, symbol: str): + self.user = bot_user self.symbol = symbol - async def get_move(self, board: dict[int, str], _: discord.Message) -> tuple[bool, int]: + @staticmethod + async def get_move(board: dict[int, str], _: discord.Message) -> tuple[bool, int]: """Get move from AI. AI use Minimax strategy.""" possible_moves = [i for i, emoji in board.items() if emoji in list(Emojis.number_emojis.values())] @@ -97,8 +99,8 @@ class AI: return False, random.choice(open_edges) def __str__(self) -> str: - """Return `AI` as user name.""" - return "AI" + """Return mention of @Sir Lancebot.""" + return self.user.mention class Game: @@ -107,6 +109,7 @@ class Game: def __init__(self, players: list[Union[Player, AI]], ctx: Context): self.players = players self.ctx = ctx + self.channel = ctx.channel self.board = { 1: Emojis.number_emojis[1], 2: Emojis.number_emojis[2], @@ -173,7 +176,8 @@ class Game: self.canceled = True return False, "User declined" - async def add_reactions(self, msg: discord.Message) -> None: + @staticmethod + async def add_reactions(msg: discord.Message) -> None: """Add number emojis to message.""" for nr in Emojis.number_emojis.values(): await msg.add_reaction(nr) @@ -265,7 +269,7 @@ class TicTacToe(Cog): return if opponent is None: game = Game( - [Player(ctx.author, ctx, Emojis.x_square), AI(Emojis.o_square)], + [Player(ctx.author, ctx, Emojis.x_square), AI(ctx.me, Emojis.o_square)], ctx ) else: diff --git a/bot/exts/fun/trivia_quiz.py b/bot/exts/fun/trivia_quiz.py index 236586b0..712c8a12 100644 --- a/bot/exts/fun/trivia_quiz.py +++ b/bot/exts/fun/trivia_quiz.py @@ -16,7 +16,7 @@ from discord.ext import commands, tasks from rapidfuzz import fuzz from bot.bot import Bot -from bot.constants import Colours, NEGATIVE_REPLIES, Roles +from bot.constants import Client, Colours, NEGATIVE_REPLIES, Roles logger = logging.getLogger(__name__) @@ -332,7 +332,7 @@ class TriviaQuiz(commands.Cog): if self.game_status[ctx.channel.id]: await ctx.send( "Game is already running... " - f"do `{self.bot.command_prefix}quiz stop`" + f"do `{Client.prefix}quiz stop`" ) return |