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 | 
