diff options
| -rw-r--r-- | bot/pagination.py | 85 | ||||
| -rw-r--r-- | bot/resources/evergreen/game_recs/chrono_trigger.json | 7 | ||||
| -rw-r--r-- | bot/resources/evergreen/game_recs/digimon_world.json | 7 | ||||
| -rw-r--r-- | bot/resources/evergreen/game_recs/doom_2.json | 7 | ||||
| -rw-r--r-- | bot/resources/evergreen/game_recs/skyrim.json | 7 | ||||
| -rw-r--r-- | bot/seasons/evergreen/minesweeper.py | 29 | ||||
| -rw-r--r-- | bot/seasons/evergreen/recommend_game.py | 51 | ||||
| -rw-r--r-- | bot/seasons/evergreen/snakes/snakes_cog.py | 24 | ||||
| -rw-r--r-- | bot/seasons/evergreen/snakes/utils.py | 16 | ||||
| -rw-r--r-- | bot/seasons/halloween/monstersurvey.py | 25 | ||||
| -rw-r--r-- | bot/seasons/valentines/be_my_valentine.py | 5 | ||||
| -rw-r--r-- | bot/utils/__init__.py | 16 | 
12 files changed, 140 insertions, 139 deletions
diff --git a/bot/pagination.py b/bot/pagination.py index e6cea41f..c12b6233 100644 --- a/bot/pagination.py +++ b/bot/pagination.py @@ -22,26 +22,16 @@ class EmptyPaginatorEmbed(Exception):  class LinePaginator(Paginator): -    """ -    A class that aids in paginating code blocks for Discord messages. - -    Attributes -    ----------- -    prefix: :class:`str` -        The prefix inserted to every page. e.g. three backticks. -    suffix: :class:`str` -        The suffix appended at the end of every page. e.g. three backticks. -    max_size: :class:`int` -        The maximum amount of codepoints allowed in a page. -    max_lines: :class:`int` -        The maximum amount of lines allowed in a page. -    """ +    """A class that aids in paginating code blocks for Discord messages."""      def __init__(self, prefix='```', suffix='```', max_size=2000, max_lines=None):          """          Overrides the Paginator.__init__ from inside discord.ext.commands. -        Allows for configuration of the maximum number of lines per page. +        `prefix` and `suffix` will be prepended and appended respectively to every page. + +        `max_size` and `max_lines` denote the maximum amount of codepoints and lines +        allowed per page.          """          self.prefix = prefix          self.suffix = suffix @@ -56,22 +46,12 @@ class LinePaginator(Paginator):          """          Adds a line to the current page. -        If the line exceeds the `max_size` then an exception is raised. +        If the line exceeds the `max_size` then a RuntimeError is raised.          Overrides the Paginator.add_line from inside discord.ext.commands in order to allow          configuration of the maximum number of lines per page. -        Parameters -        ----------- -        line: str -            The line to add. -        empty: bool -            Indicates if another empty line should be added. - -        Raises -        ------ -        RuntimeError -            The line was too big for the current `max_size`. +        If `empty` is True, an empty line will be placed after the a given `line`.          """          if len(line) > self.max_size - len(self.prefix) - 2:              raise RuntimeError('Line exceeds maximum page size %s' % (self.max_size - len(self.prefix) - 2)) @@ -105,7 +85,11 @@ class LinePaginator(Paginator):          When used, this will send a message using `ctx.send()` and apply a set of reactions to it.          These reactions may be used to change page, or to remove pagination from the message. -        Pagination will also be removed automatically if no reaction is added for five minutes (300 seconds). + +        Pagination will also be removed automatically if no reaction is added for `timeout` seconds, +        defaulting to five minutes (300 seconds). + +        If `empty` is True, an empty line will be placed between each given line.          >>> embed = Embed()          >>> embed.set_author(name="Some Operation", url=url, icon_url=icon) @@ -113,20 +97,6 @@ class LinePaginator(Paginator):          ...     (line for line in lines),          ...     ctx, embed          ... ) - -        :param lines: The lines to be paginated -        :param ctx: Current context object -        :param embed: A pre-configured embed to be used as a template for each page -        :param prefix: Text to place before each page -        :param suffix: Text to place after each page -        :param max_lines: The maximum number of lines on each page -        :param max_size: The maximum number of characters on each page -        :param empty: Whether to place an empty line between each given line -        :param restrict_to_user: A user to lock pagination operations to for this message, if supplied -        :param exception_on_empty_embed: Should there be an exception if the embed is empty? -        :param url: the url to use for the embed headline -        :param timeout: The amount of time in seconds to disable pagination of no reaction is added -        :param footer_text: Text to prefix the page number in the footer with          """          def event_check(reaction_: Reaction, user_: Member):              """Make sure that this reaction is what we want to operate on.""" @@ -314,8 +284,7 @@ class ImagePaginator(Paginator):          """          Adds a line to each page, usually just 1 line in this context. -        :param line: str to be page content / title -        :param empty: if there should be new lines between entries +        If `empty` is True, an empty line will be placed after a given `line`.          """          if line:              self._count = len(line) @@ -325,11 +294,7 @@ class ImagePaginator(Paginator):          self.close_page()      def add_image(self, image: str = None) -> None: -        """ -        Adds an image to a page. - -        :param image: image url to be appended -        """ +        """Adds an image to a page given the url."""          self.images.append(image)      @classmethod @@ -339,33 +304,21 @@ class ImagePaginator(Paginator):          """          Use a paginator and set of reactions to provide pagination over a set of title/image pairs. -        The reactions are used to switch page, or to finish with pagination. +        `pages` is a list of tuples of page title/image url pairs. +        `prefix` and `suffix` will be prepended and appended respectively to the message.          When used, this will send a message using `ctx.send()` and apply a set of reactions to it.          These reactions may be used to change page, or to remove pagination from the message. -        Note: Pagination will be removed automatically if no reaction is added for five minutes (300 seconds). +        Note: Pagination will be removed automatically if no reaction is added for `timeout` seconds, +              defaulting to five minutes (300 seconds).          >>> embed = Embed()          >>> embed.set_author(name="Some Operation", url=url, icon_url=icon)          >>> await ImagePaginator.paginate(pages, ctx, embed) - -        Parameters -        ----------- -        :param pages: An iterable of tuples with title for page, and img url -        :param ctx: ctx for message -        :param embed: base embed to modify -        :param prefix: prefix of message -        :param suffix: suffix of message -        :param timeout: timeout for when reactions get auto-removed          """          def check_event(reaction_: Reaction, member: Member) -> bool: -            """ -            Checks each reaction added, if it matches our conditions pass the wait_for. - -            :param reaction_: reaction added -            :param member: reaction added by member -            """ +            """Checks each reaction added, if it matches our conditions pass the wait_for."""              return all((                  # Reaction is on the same message sent                  reaction_.message.id == message.id, diff --git a/bot/resources/evergreen/game_recs/chrono_trigger.json b/bot/resources/evergreen/game_recs/chrono_trigger.json new file mode 100644 index 00000000..219c1e78 --- /dev/null +++ b/bot/resources/evergreen/game_recs/chrono_trigger.json @@ -0,0 +1,7 @@ +{ +  "title": "Chrono Trigger", +  "description": "One of the best games of all time. A brilliant story involving time-travel with loveable characters. It has a brilliant score by Yasonuri Mitsuda and artwork by Akira Toriyama. With over 20 endings and New Game+, there is a huge amount of replay value here.", +  "link": "https://rawg.io/games/chrono-trigger-1995", +  "image": "https://vignette.wikia.nocookie.net/chrono/images/2/24/Chrono_Trigger_cover.jpg", +  "author": "352635617709916161" +}
\ No newline at end of file diff --git a/bot/resources/evergreen/game_recs/digimon_world.json b/bot/resources/evergreen/game_recs/digimon_world.json new file mode 100644 index 00000000..a2820f8e --- /dev/null +++ b/bot/resources/evergreen/game_recs/digimon_world.json @@ -0,0 +1,7 @@ +{ +  "title": "Digimon World", +  "description": "A great mix of town-building and pet-raising set in the Digimon universe. With plenty of Digimon to raise and recruit to the village, this charming game will keep you occupied for a long time.", +  "image": "https://www.mobygames.com/images/covers/l/437308-digimon-world-playstation-front-cover.jpg", +  "link": "https://rawg.io/games/digimon-world", +  "author": "352635617709916161" +}
\ No newline at end of file diff --git a/bot/resources/evergreen/game_recs/doom_2.json b/bot/resources/evergreen/game_recs/doom_2.json new file mode 100644 index 00000000..e228e2b1 --- /dev/null +++ b/bot/resources/evergreen/game_recs/doom_2.json @@ -0,0 +1,7 @@ +{ +  "title": "Doom II", +  "description": "Doom 2 was one of the first FPS games that I truly enjoyed. It offered awesome weapons, terrifying demons to kill, and a great atmosphere to do it in.", +  "image": "https://upload.wikimedia.org/wikipedia/en/thumb/2/29/Doom_II_-_Hell_on_Earth_Coverart.png/220px-Doom_II_-_Hell_on_Earth_Coverart.png", +  "link": "https://rawg.io/games/doom-ii", +  "author": "352635617709916161" +}
\ No newline at end of file diff --git a/bot/resources/evergreen/game_recs/skyrim.json b/bot/resources/evergreen/game_recs/skyrim.json new file mode 100644 index 00000000..09f93563 --- /dev/null +++ b/bot/resources/evergreen/game_recs/skyrim.json @@ -0,0 +1,7 @@ +{ +  "title": "Elder Scrolls V: Skyrim", +  "description": "The latest mainline Elder Scrolls game offered a fantastic role-playing experience with untethered freedom and a great story. Offering vast mod support, the game has endless customization and replay value.", +  "image": "https://upload.wikimedia.org/wikipedia/en/1/15/The_Elder_Scrolls_V_Skyrim_cover.png", +  "link": "https://rawg.io/games/the-elder-scrolls-v-skyrim", +  "author": "352635617709916161" +}
\ No newline at end of file diff --git a/bot/seasons/evergreen/minesweeper.py b/bot/seasons/evergreen/minesweeper.py index 9f6aff95..cb859ea9 100644 --- a/bot/seasons/evergreen/minesweeper.py +++ b/bot/seasons/evergreen/minesweeper.py @@ -1,7 +1,7 @@  import logging  import typing  from dataclasses import dataclass -from random import random +from random import randint, random  import discord  from discord.ext import commands @@ -22,7 +22,8 @@ MESSAGE_MAPPING = {      10: ":keycap_ten:",      "bomb": ":bomb:",      "hidden": ":grey_question:", -    "flag": ":pyflag:" +    "flag": ":flag_black:", +    "x": ":x:"  }  log = logging.getLogger(__name__) @@ -36,7 +37,13 @@ class CoordinateConverter(commands.Converter):          if not 2 <= len(coordinate) <= 3:              raise commands.BadArgument('Invalid co-ordinate provided') -        digit, letter = sorted(coordinate.lower()) +        coordinate = coordinate.lower() +        if coordinate[0].isalpha(): +            digit = coordinate[1:] +            letter = coordinate[0] +        else: +            digit = coordinate[:-1] +            letter = coordinate[-1]          if not digit.isdigit():              raise commands.BadArgument @@ -93,6 +100,10 @@ class Minesweeper(commands.Cog):                  for _ in range(10)              ] for _ in range(10)          ] + +        # make sure there is always a free cell +        board[randint(0, 9)][randint(0, 9)] = "number" +          for y, row in enumerate(board):              for x, cell in enumerate(row):                  if cell == "number": @@ -172,10 +183,18 @@ class Minesweeper(commands.Cog):          await self.update_boards(ctx) +    @staticmethod +    def reveal_bombs(revealed: GameBoard, board: GameBoard) -> None: +        """Reveals all the bombs""" +        for y, row in enumerate(board): +            for x, cell in enumerate(row): +                if cell == "bomb": +                    revealed[y][x] = cell +      async def lost(self, ctx: commands.Context) -> None:          """The player lost the game"""          game = self.games[ctx.author.id] -        game.revealed = game.board +        self.reveal_bombs(game.revealed, game.board)          await ctx.author.send(":fire: You lost! :fire:")          if game.activated_on_server:              await game.chat_msg.channel.send(f":fire: {ctx.author.mention} just lost Minesweeper! :fire:") @@ -183,7 +202,6 @@ class Minesweeper(commands.Cog):      async def won(self, ctx: commands.Context) -> None:          """The player won the game"""          game = self.games[ctx.author.id] -        game.revealed = game.board          await ctx.author.send(":tada: You won! :tada:")          if game.activated_on_server:              await game.chat_msg.channel.send(f":tada: {ctx.author.mention} just won Minesweeper! :tada:") @@ -225,6 +243,7 @@ class Minesweeper(commands.Cog):          revealed[y][x] = board[y][x]          if board[y][x] == "bomb":              await self.lost(ctx) +            revealed[y][x] = "x"  # mark bomb that made you lose with a x              return True          elif board[y][x] == 0:              self.reveal_zeros(revealed, board, x, y) diff --git a/bot/seasons/evergreen/recommend_game.py b/bot/seasons/evergreen/recommend_game.py new file mode 100644 index 00000000..835a4e53 --- /dev/null +++ b/bot/seasons/evergreen/recommend_game.py @@ -0,0 +1,51 @@ +import json +import logging +from pathlib import Path +from random import shuffle + +import discord +from discord.ext import commands + +log = logging.getLogger(__name__) +game_recs = [] + +# Populate the list `game_recs` with resource files +for rec_path in Path("bot/resources/evergreen/game_recs").glob("*.json"): +    with rec_path.open(encoding='utf-8') as file: +        data = json.load(file) +    game_recs.append(data) +shuffle(game_recs) + + +class RecommendGame(commands.Cog): +    """Commands related to recommending games.""" + +    def __init__(self, bot: commands.Bot) -> None: +        self.bot = bot +        self.index = 0 + +    @commands.command(name="recommendgame", aliases=['gamerec']) +    async def recommend_game(self, ctx: commands.Context) -> None: +        """Sends an Embed of a random game recommendation.""" +        if self.index >= len(game_recs): +            self.index = 0 +            shuffle(game_recs) +        game = game_recs[self.index] +        self.index += 1 + +        author = self.bot.get_user(int(game['author'])) + +        # Creating and formatting Embed +        embed = discord.Embed(color=discord.Colour.blue()) +        if author is not None: +            embed.set_author(name=author.name, icon_url=author.avatar_url) +        embed.set_image(url=game['image']) +        embed.add_field(name='Recommendation: ' + game['title'] + '\n' + game['link'], value=game['description']) + +        await ctx.send(embed=embed) + + +def setup(bot: commands.Bot) -> None: +    """Loads the RecommendGame cog.""" +    bot.add_cog(RecommendGame(bot)) +    log.info("RecommendGame cog loaded") diff --git a/bot/seasons/evergreen/snakes/snakes_cog.py b/bot/seasons/evergreen/snakes/snakes_cog.py index 1d138aff..38878706 100644 --- a/bot/seasons/evergreen/snakes/snakes_cog.py +++ b/bot/seasons/evergreen/snakes/snakes_cog.py @@ -303,9 +303,6 @@ class Snakes(Cog):          Builds a dict that the .get() method can use.          Created by Ava and eivl. - -        :param name: The name of the snake to get information for - omit for a random snake -        :return: A dict containing information on a snake          """          snake_info = {} @@ -403,11 +400,7 @@ class Snakes(Cog):          return snake_info      async def _get_snake_name(self) -> Dict[str, str]: -        """ -        Gets a random snake name. - -        :return: A random snake name, as a string. -        """ +        """Gets a random snake name."""          return random.choice(self.snake_names)      async def _validate_answer(self, ctx: Context, message: Message, answer: str, options: list): @@ -631,14 +624,10 @@ class Snakes(Cog):      @snakes_group.command(name='get')      @bot_has_permissions(manage_messages=True)      @locked() -    async def get_command(self, ctx: Context, *, name: Snake = None): +    async def get_command(self, ctx: Context, *, name: Snake = None) -> None:          """          Fetches information about a snake from Wikipedia. -        :param ctx: Context object passed from discord.py -        :param name: Optional, the name of the snake to get information -                     for - omit for a random snake -          Created by Ava and eivl.          """          with ctx.typing(): @@ -1034,10 +1023,8 @@ class Snakes(Cog):          """          How would I talk if I were a snake? -        :param ctx: context -        :param message: If this is passed, it will snakify the message. -                        If not, it will snakify a random message from -                        the users history. +        If `message` is passed, the bot will snakify the message. +        Otherwise, a random message from the user's history is snakified.          Written by Momo and kel.          Modified by lemon. @@ -1077,8 +1064,7 @@ class Snakes(Cog):          """          Gets a YouTube video about snakes. -        :param ctx: Context object passed from discord.py -        :param search: Optional, a name of a snake. Used to search for videos with that name +        If `search` is given, a snake with that name will be searched on Youtube.          Written by Andrew and Prithaj.          """ diff --git a/bot/seasons/evergreen/snakes/utils.py b/bot/seasons/evergreen/snakes/utils.py index 5d3b0dee..e8d2ee44 100644 --- a/bot/seasons/evergreen/snakes/utils.py +++ b/bot/seasons/evergreen/snakes/utils.py @@ -285,20 +285,8 @@ def create_snek_frame(      """      Creates a single random snek frame using Perlin noise. -    :param perlin_factory: the perlin noise factory used. Required. -    :param perlin_lookup_vertical_shift: the Perlin noise shift in the Y-dimension for this frame -    :param image_dimensions: the size of the output image. -    :param image_margins: the margins to respect inside of the image. -    :param snake_length: the length of the snake, in segments. -    :param snake_color: the color of the snake. -    :param bg_color: the background color. -    :param segment_length_range: the range of the segment length. Values will be generated inside -                                 this range, including the bounds. -    :param snake_width: the width of the snek, in pixels. -    :param text: the text to display with the snek. Set to None for no text. -    :param text_position: the position of the text. -    :param text_color: the color of the text. -    :return: a PIL image, representing a single frame. +    `perlin_lookup_vertical_shift` represents the Perlin noise shift in the Y-dimension for this frame. +    If `text` is given, display the given text with the snek.      """      start_x = random.randint(image_margins[X], image_dimensions[X] - image_margins[X])      start_y = random.randint(image_margins[Y], image_dimensions[Y] - image_margins[Y]) diff --git a/bot/seasons/halloween/monstersurvey.py b/bot/seasons/halloween/monstersurvey.py index 4e967cca..173ce8eb 100644 --- a/bot/seasons/halloween/monstersurvey.py +++ b/bot/seasons/halloween/monstersurvey.py @@ -36,15 +36,11 @@ class MonsterSurvey(Cog):          with open(self.registry_location, 'w') as jason:              json.dump(self.voter_registry, jason, indent=2) -    def cast_vote(self, id: int, monster: str): +    def cast_vote(self, id: int, monster: str) -> None:          """          Cast a user's vote for the specified monster.          If the user has already voted, their existing vote is removed. - -        :param id: The id of the person voting -        :param monster: the string key of the json that represents a monster -        :return: None          """          vr = self.voter_registry          for m in vr.keys(): @@ -147,14 +143,8 @@ class MonsterSurvey(Cog):      @monster_group.command(          name='show'      ) -    async def monster_show(self, ctx: Context, name=None): -        """ -        Shows the named monster. If one is not named, it sends the default voting embed instead. - -        :param ctx: -        :param name: -        :return: -        """ +    async def monster_show(self, ctx: Context, name=None) -> None: +        """Shows the named monster. If one is not named, it sends the default voting embed instead."""          if name is None:              await ctx.invoke(self.monster_leaderboard)              return @@ -184,13 +174,8 @@ class MonsterSurvey(Cog):          name='leaderboard',          aliases=('lb',)      ) -    async def monster_leaderboard(self, ctx: Context): -        """ -        Shows the current standings. - -        :param ctx: -        :return: -        """ +    async def monster_leaderboard(self, ctx: Context) -> None: +        """Shows the current standings."""          async with ctx.typing():              vr = self.voter_registry              top = sorted(vr, key=lambda k: len(vr[k]['votes']), reverse=True) diff --git a/bot/seasons/valentines/be_my_valentine.py b/bot/seasons/valentines/be_my_valentine.py index ac140896..c4acf17a 100644 --- a/bot/seasons/valentines/be_my_valentine.py +++ b/bot/seasons/valentines/be_my_valentine.py @@ -184,14 +184,11 @@ class BeMyValentine(commands.Cog):          return valentine, title      @staticmethod -    def random_user(author, members): +    def random_user(author: discord.Member, members: discord.Member):          """          Picks a random member from the list provided in `members`.          The invoking author is ignored. - -        :param author: member who invoked the command -        :param members: list of discord.Member objects          """          if author in members:              members.remove(author) diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py index ad019357..3249a9cf 100644 --- a/bot/utils/__init__.py +++ b/bot/utils/__init__.py @@ -11,21 +11,15 @@ from bot.pagination import LinePaginator  async def disambiguate(          ctx: Context, entries: List[str], *, timeout: float = 30, -        per_page: int = 20, empty: bool = False, embed: discord.Embed = None -): +        entries_per_page: int = 20, empty: bool = False, embed: discord.Embed = None +) -> str:      """      Has the user choose between multiple entries in case one could not be chosen automatically. +    Disambiguation will be canceled after `timeout` seconds. +      This will raise a BadArgument if entries is empty, if the disambiguation event times out,      or if the user makes an invalid choice. - -    :param ctx: Context object from discord.py -    :param entries: List of items for user to choose from -    :param timeout: Number of seconds to wait before canceling disambiguation -    :param per_page: Entries per embed page -    :param empty: Whether the paginator should have an extra line between items -    :param embed: The embed that the paginator will use. -    :return: Users choice for correct entry.      """      if len(entries) == 0:          raise BadArgument('No matches found.') @@ -45,7 +39,7 @@ async def disambiguate(              embed = discord.Embed()          coro1 = ctx.bot.wait_for('message', check=check, timeout=timeout) -        coro2 = LinePaginator.paginate(choices, ctx, embed=embed, max_lines=per_page, +        coro2 = LinePaginator.paginate(choices, ctx, embed=embed, max_lines=entries_per_page,                                         empty=empty, max_size=6000, timeout=9000)          # wait_for timeout will go to except instead of the wait_for thing as I expected  |