diff options
Diffstat (limited to 'bot/seasons')
| -rw-r--r-- | bot/seasons/evergreen/__init__.py | 2 | ||||
| -rw-r--r-- | bot/seasons/evergreen/error_handler.py | 6 | ||||
| -rw-r--r-- | bot/seasons/evergreen/fun.py | 12 | ||||
| -rw-r--r-- | bot/seasons/evergreen/snakes/__init__.py | 2 | ||||
| -rw-r--r-- | bot/seasons/evergreen/snakes/converter.py | 15 | ||||
| -rw-r--r-- | bot/seasons/evergreen/snakes/snakes_cog.py | 99 | ||||
| -rw-r--r-- | bot/seasons/evergreen/snakes/utils.py | 119 | ||||
| -rw-r--r-- | bot/seasons/evergreen/uptime.py | 12 | 
8 files changed, 167 insertions, 100 deletions
| diff --git a/bot/seasons/evergreen/__init__.py b/bot/seasons/evergreen/__init__.py index db610e7c..ac32c199 100644 --- a/bot/seasons/evergreen/__init__.py +++ b/bot/seasons/evergreen/__init__.py @@ -2,4 +2,6 @@ from bot.seasons import SeasonBase  class Evergreen(SeasonBase): +    """Evergreen Seasonal event attributes.""" +      bot_icon = "/logos/logo_seasonal/evergreen/logo_evergreen.png" diff --git a/bot/seasons/evergreen/error_handler.py b/bot/seasons/evergreen/error_handler.py index 47e18a31..dcdbe4e9 100644 --- a/bot/seasons/evergreen/error_handler.py +++ b/bot/seasons/evergreen/error_handler.py @@ -9,13 +9,13 @@ log = logging.getLogger(__name__)  class CommandErrorHandler:
 -    """A error handler for the PythonDiscord server!"""
 +    """A error handler for the PythonDiscord server."""
      def __init__(self, bot):
          self.bot = bot
      async def on_command_error(self, ctx, error):
 -        """Activates when a command opens an error"""
 +        """Activates when a command opens an error."""
          if hasattr(ctx.command, 'on_error'):
              return logging.debug(
 @@ -108,5 +108,7 @@ class CommandErrorHandler:  def setup(bot):
 +    """Error handler Cog load."""
 +
      bot.add_cog(CommandErrorHandler(bot))
      log.debug("CommandErrorHandler cog loaded")
 diff --git a/bot/seasons/evergreen/fun.py b/bot/seasons/evergreen/fun.py index 4da01dd1..f5814a80 100644 --- a/bot/seasons/evergreen/fun.py +++ b/bot/seasons/evergreen/fun.py @@ -9,18 +9,15 @@ log = logging.getLogger(__name__)  class Fun: -    """ -    A collection of general commands for fun. -    """ +    """A collection of general commands for fun."""      def __init__(self, bot):          self.bot = bot      @commands.command()      async def roll(self, ctx, num_rolls: int = 1): -        """ -            Outputs a number of random dice emotes (up to 6) -        """ +        """Outputs a number of random dice emotes (up to 6).""" +          output = ""          if num_rolls > 6:              num_rolls = 6 @@ -32,7 +29,8 @@ class Fun:          await ctx.send(output) -# Required in order to load the cog, use the class name in the add_cog function.  def setup(bot): +    """Fun Cog load.""" +      bot.add_cog(Fun(bot))      log.debug("Fun cog loaded") diff --git a/bot/seasons/evergreen/snakes/__init__.py b/bot/seasons/evergreen/snakes/__init__.py index 367aea4d..88793308 100644 --- a/bot/seasons/evergreen/snakes/__init__.py +++ b/bot/seasons/evergreen/snakes/__init__.py @@ -6,5 +6,7 @@ log = logging.getLogger(__name__)  def setup(bot): +    """Snakes Cog load.""" +      bot.add_cog(Snakes(bot))      log.info("Cog loaded: Snakes") diff --git a/bot/seasons/evergreen/snakes/converter.py b/bot/seasons/evergreen/snakes/converter.py index c091d9c1..ec9c9870 100644 --- a/bot/seasons/evergreen/snakes/converter.py +++ b/bot/seasons/evergreen/snakes/converter.py @@ -13,10 +13,14 @@ log = logging.getLogger(__name__)  class Snake(Converter): +    """Snake converter for the Snakes Cog.""" +      snakes = None      special_cases = None      async def convert(self, ctx, name): +        """Convert the input snake name to the closest matching Snake object.""" +          await self.build_list()          name = name.lower() @@ -56,6 +60,8 @@ class Snake(Converter):      @classmethod      async def build_list(cls): +        """Build list of snakes from the static snake resources.""" +          # Get all the snakes          if cls.snakes is None:              with (SNAKE_RESOURCES / "snake_names.json").open() as snakefile: @@ -70,11 +76,14 @@ class Snake(Converter):      @classmethod      async def random(cls):          """ -        This is stupid. We should find a way to -        somehow get the global session into a -        global context, so I can get it from here. +        Get a random Snake from the loaded resources. + +        This is stupid. We should find a way to somehow get the global session into a global context, +        so I can get it from here. +          :return:          """ +          await cls.build_list()          names = [snake['scientific'] for snake in cls.snakes]          return random.choice(names) diff --git a/bot/seasons/evergreen/snakes/snakes_cog.py b/bot/seasons/evergreen/snakes/snakes_cog.py index 57eb7a52..92b28c55 100644 --- a/bot/seasons/evergreen/snakes/snakes_cog.py +++ b/bot/seasons/evergreen/snakes/snakes_cog.py @@ -134,12 +134,11 @@ CARD = {  class Snakes:      """ -    Commands related to snakes. These were created by our -    community during the first code jam. +    Commands related to snakes, created by our community during the first code jam.      More information can be found in the code-jam-1 repo. -    https://gitlab_bot_repo.com/discord-python/code-jams/code-jam-1 +    https://github.com/python-discord/code-jam-1      """      wiki_brief = re.compile(r'(.*?)(=+ (.*?) =+)', flags=re.DOTALL) @@ -156,9 +155,8 @@ class Snakes:      # region: Helper methods      @staticmethod      def _beautiful_pastel(hue): -        """ -        Returns random bright pastels. -        """ +        """Returns random bright pastels.""" +          light = random.uniform(0.7, 0.85)          saturation = 1 @@ -178,6 +176,7 @@ class Snakes:          Written by juan and Someone during the first code jam.          """ +          snake = Image.open(buffer)          # Get the size of the snake icon, configure the height of the image box (yes, it changes) @@ -254,9 +253,8 @@ class Snakes:      @staticmethod      def _snakify(message): -        """ -        Sssnakifffiesss a sstring. -        """ +        """Sssnakifffiesss a sstring.""" +          # Replace fricatives with exaggerated snake fricatives.          simple_fricatives = [              "f", "s", "z", "h", @@ -278,9 +276,8 @@ class Snakes:          return message      async def _fetch(self, session, url, params=None): -        """ -        Asyncronous web request helper method. -        """ +        """Asyncronous web request helper method.""" +          if params is None:              params = {} @@ -290,11 +287,11 @@ class Snakes:      def _get_random_long_message(self, messages, retries=10):          """ -        Fetch a message that's at least 3 words long, -        but only if it is possible to do so in retries -        attempts. Else, just return whatever the last -        message is. +        Fetch a message that's at least 3 words long, if possible to do so in retries attempts. + +        Else, just return whatever the last message is.          """ +          long_message = random.choice(messages)          if len(long_message.split()) < 3 and retries > 0:              return self._get_random_long_message( @@ -306,14 +303,16 @@ class Snakes:      async def _get_snek(self, name: str) -> Dict[str, Any]:          """ -        Goes online and fetches all the data from a wikipedia article -        about a snake. Builds a dict that the .get() method can use. +        Fetches all the data from a wikipedia article about a snake. + +        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 = {}          async with aiohttp.ClientSession() as session: @@ -412,20 +411,21 @@ class Snakes:      async def _get_snake_name(self) -> Dict[str, str]:          """          Gets a random snake name. +          :return: A random snake name, as a string.          """          return random.choice(self.snake_names)      async def _validate_answer(self, ctx: Context, message: Message, answer: str, options: list):          """ -        Validate the answer using a reaction event loop +        Validate the answer using a reaction event loop. +          :return:          """          def predicate(reaction, user): -            """ -            Test if the the answer is valid and can be evaluated. -            """ +            """Test if the the answer is valid and can be evaluated.""" +              return (                  reaction.message.id == message.id                  # The reaction is attached to the question we asked.                  and user == ctx.author                             # It's the user who triggered the quiz. @@ -465,7 +465,7 @@ class Snakes:      @locked()      async def antidote_command(self, ctx: Context):          """ -        Antidote - Can you create the antivenom before the patient dies? +        Antidote - Can you create the antivenom before the patient dies.          Rules:  You have 4 ingredients for each antidote, you only have 10 attempts                  Once you synthesize the antidote, you will be presented with 4 markers @@ -480,9 +480,7 @@ class Snakes:          """          def predicate(reaction_: Reaction, user_: Member): -            """ -            Make sure that this reaction is what we want to operate on -            """ +            """Make sure that this reaction is what we want to operate on."""              return (                  all(( @@ -610,7 +608,7 @@ class Snakes:      @snakes_group.command(name='draw')      async def draw_command(self, ctx: Context):          """ -        Draws a random snek using Perlin noise +        Draws a random snek using Perlin noise.          Written by Momo and kel.          Modified by juan and lemon. @@ -652,12 +650,14 @@ class Snakes:      async def get_command(self, ctx: Context, *, name: Snake = 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():              if name is None:                  name = await Snake.random() @@ -702,11 +702,12 @@ class Snakes:      @locked()      async def guess_command(self, ctx):          """ -        Snake identifying game! +        Snake identifying game.          Made by Ava and eivl.          Modified by lemon.          """ +          with ctx.typing():              image = None @@ -736,10 +737,11 @@ class Snakes:      @snakes_group.command(name='hatch')      async def hatch_command(self, ctx: Context):          """ -        Hatches your personal snake +        Hatches your personal snake.          Written by Momo and kel.          """ +          # Pick a random snake to hatch.          snake_name = random.choice(list(utils.snakes.keys()))          snake_image = utils.snakes[snake_name] @@ -772,6 +774,7 @@ class Snakes:          Written by Samuel.          Modified by gdude.          """ +          url = "http://www.omdbapi.com/"          page = random.randint(1, 27) @@ -842,6 +845,7 @@ class Snakes:          This was created by Mushy and Cardium,          and modified by Urthas and lemon.          """ +          # Prepare a question.          question = random.choice(self.snake_quizzes)          answer = question["answerkey"] @@ -862,6 +866,8 @@ class Snakes:      @snakes_group.command(name='name', aliases=('name_gen',))      async def name_command(self, ctx: Context, *, name: str = None):          """ +        Snakifies a username. +          Slices the users name at the last vowel (or second last if the name          ends with a vowel), and then combines it with a random snake name,          which is sliced at the first vowel (or second if the name starts with @@ -880,6 +886,7 @@ class Snakes:          This was written by Iceman, and modified for inclusion into the bot by lemon.          """ +          snake_name = await self._get_snake_name()          snake_name = snake_name['name']          snake_prefix = "" @@ -932,11 +939,12 @@ class Snakes:      @locked()      async def sal_command(self, ctx: Context):          """ -        Play a game of Snakes and Ladders! +        Play a game of Snakes and Ladders.          Written by Momo and kel.          Modified by lemon.          """ +          # check if there is already a game in this channel          if ctx.channel in self.active_sal:              await ctx.send(f"{ctx.author.mention} A game is already in progress in this channel.") @@ -949,10 +957,8 @@ class Snakes:      @snakes_group.command(name='about')      async def about_command(self, ctx: Context): -        """ -        A command that shows an embed with information about the event, -        it's participants, and its winners. -        """ +        """Show an embed with information about the event, its participants, and its winners.""" +          contributors = [              "<@!245270749919576066>",              "<@!396290259907903491>", @@ -996,10 +1002,11 @@ class Snakes:      @snakes_group.command(name='card')      async def card_command(self, ctx: Context, *, name: Snake = None):          """ -        Create an interesting little card from a snake! +        Create an interesting little card from a snake.          Created by juan and Someone during the first code jam.          """ +          # Get the snake data we need          if not name:              name_obj = await self._get_snake_name() @@ -1034,11 +1041,12 @@ class Snakes:      @snakes_group.command(name='fact')      async def fact_command(self, ctx: Context):          """ -        Gets a snake-related fact +        Gets a snake-related fact.          Written by Andrew and Prithaj.          Modified by lemon.          """ +          question = random.choice(self.snake_facts)["fact"]          embed = Embed(              title="Snake fact", @@ -1049,16 +1057,16 @@ class Snakes:      @snakes_group.command(name='help')      async def help_command(self, ctx: Context): -        """ -        This just invokes the help command on this cog. -        """ +        """Invokes the help command for the Snakes Cog.""" +          log.debug(f"{ctx.author} requested info about the snakes cog")          return await ctx.invoke(self.bot.get_command("help"), "Snakes")      @snakes_group.command(name='snakify')      async def snakify_command(self, ctx: Context, *, message: str = None):          """ -        How would I talk if I were a snake? +        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 @@ -1067,6 +1075,7 @@ class Snakes:          Written by Momo and kel.          Modified by lemon.          """ +          with ctx.typing():              embed = Embed()              user = ctx.message.author @@ -1100,13 +1109,14 @@ class Snakes:      @snakes_group.command(name='video', aliases=('get_video',))      async def video_command(self, ctx: Context, *, search: str = None):          """ -        Gets a YouTube video about snakes +        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          Written by Andrew and Prithaj.          """ +          # Are we searching for anything specific?          if search:              query = search + ' snake' @@ -1141,12 +1151,12 @@ class Snakes:      @snakes_group.command(name='zen')      async def zen_command(self, ctx: Context):          """ -        Gets a random quote from the Zen of Python, -        except as if spoken by a snake. +        Gets a random quote from the Zen of Python, except as if spoken by a snake.          Written by Prithaj and Andrew.          Modified by lemon.          """ +          embed = Embed(              title="Zzzen of Pythhon",              color=SNAKE_COLOR @@ -1168,6 +1178,7 @@ class Snakes:      @card_command.error      @video_command.error      async def command_error(self, ctx, error): +        """Local error handler for the Snake Cog."""          embed = Embed()          embed.colour = Colour.red() @@ -1190,5 +1201,7 @@ class Snakes:  def setup(bot): +    """Snake Cog load.""" +      bot.add_cog(Snakes(bot))      log.info("Cog loaded: Snakes") diff --git a/bot/seasons/evergreen/snakes/utils.py b/bot/seasons/evergreen/snakes/utils.py index 605c7ef3..ba2068d5 100644 --- a/bot/seasons/evergreen/snakes/utils.py +++ b/bot/seasons/evergreen/snakes/utils.py @@ -1,8 +1,3 @@ -""" -Perlin noise implementation. -Taken from: https://gist.github.com/eevee/26f547457522755cb1fb8739d0ea89a1 -Licensed under ISC -"""  import asyncio  import io  import json @@ -117,43 +112,54 @@ ANGLE_RANGE = math.pi * 2  def get_resource(file: str) -> List[dict]: +    """Load Snake resources JSON.""" +      with (SNAKE_RESOURCES / f"{file}.json").open() as snakefile:          return json.load(snakefile)  def smoothstep(t): -    """Smooth curve with a zero derivative at 0 and 1, making it useful for -    interpolating. -    """ +    """Smooth curve with a zero derivative at 0 and 1, making it useful for interpolating.""" +      return t * t * (3. - 2. * t)  def lerp(t, a, b):      """Linear interpolation between a and b, given a fraction t.""" +      return a + t * (b - a)  class PerlinNoiseFactory(object): -    """Callable that produces Perlin noise for an arbitrary point in an -    arbitrary number of dimensions.  The underlying grid is aligned with the -    integers. -    There is no limit to the coordinates used; new gradients are generated on -    the fly as necessary. +    """ +    Callable that produces Perlin noise for an arbitrary point in an arbitrary number of dimensions. + +    The underlying grid is aligned with the integers. + +    There is no limit to the coordinates used; new gradients are generated on the fly as necessary. + +    Taken from: https://gist.github.com/eevee/26f547457522755cb1fb8739d0ea89a1 +    Licensed under ISC      """      def __init__(self, dimension, octaves=1, tile=(), unbias=False): -        """Create a new Perlin noise factory in the given number of dimensions, -        which should be an integer and at least 1. -        More octaves create a foggier and more-detailed noise pattern.  More -        than 4 octaves is rather excessive. -        ``tile`` can be used to make a seamlessly tiling pattern.  For example: +        """ +        Create a new Perlin noise factory in the given number of dimensions. + +        dimension should be an integer and at least 1. + +        More octaves create a foggier and more-detailed noise pattern.  More than 4 octaves is rather excessive. + +        ``tile`` can be used to make a seamlessly tiling pattern. +        For example:              pnf = PerlinNoiseFactory(2, tile=(0, 3)) -        This will produce noise that tiles every 3 units vertically, but never -        tiles horizontally. -        If ``unbias`` is true, the smoothstep function will be applied to the -        output before returning it, to counteract some of Perlin noise's -        significant bias towards the center of its output range. + +        This will produce noise that tiles every 3 units vertically, but never tiles horizontally. + +        If ``unbias`` is true, the smoothstep function will be applied to the output before returning +        it, to counteract some of Perlin noise's significant bias towards the center of its output range.          """ +          self.dimension = dimension          self.octaves = octaves          self.tile = tile + (0,) * dimension @@ -166,8 +172,11 @@ class PerlinNoiseFactory(object):          self.gradient = {}      def _generate_gradient(self): -        # Generate a random unit vector at each grid point -- this is the -        # "gradient" vector, in that the grid tile slopes towards it +        """ +        Generate a random unit vector at each grid point. + +        This is the "gradient" vector, in that the grid tile slopes towards it +        """          # 1 dimension is special, since the only unit vector is trivial;          # instead, use a slope between -1 and 1 @@ -184,9 +193,8 @@ class PerlinNoiseFactory(object):          return tuple(coord * scale for coord in random_point)      def get_plain_noise(self, *point): -        """Get plain noise for a single point, without taking into account -        either octaves or tiling. -        """ +        """Get plain noise for a single point, without taking into account either octaves or tiling.""" +          if len(point) != self.dimension:              raise ValueError("Expected {0} values, got {1}".format(                  self.dimension, len(point))) @@ -234,9 +242,12 @@ class PerlinNoiseFactory(object):          return dots[0] * self.scale_factor      def __call__(self, *point): -        """Get the value of this Perlin noise function at the given point.  The -        number of values given should match the number of dimensions.          """ +        Get the value of this Perlin noise function at the given point. + +        The number of values given should match the number of dimensions. +        """ +          ret = 0          for o in range(self.octaves):              o2 = 1 << o @@ -281,6 +292,7 @@ def create_snek_frame(  ) -> Image:      """      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. @@ -288,14 +300,15 @@ def create_snek_frame(      :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 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.      """ +      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])      points = [(start_x, start_y)] @@ -349,6 +362,8 @@ def create_snek_frame(  def frame_to_png_bytes(image: Image): +    """Convert image to byte stream.""" +      stream = io.BytesIO()      image.save(stream, format='PNG')      return stream.getvalue() @@ -371,6 +386,8 @@ GAME_SCREEN_EMOJI = [  class SnakeAndLaddersGame: +    """Snakes and Ladders game Cog.""" +      def __init__(self, snakes, context: Context):          self.snakes = snakes          self.ctx = context @@ -393,10 +410,10 @@ class SnakeAndLaddersGame:          Listen for reactions until players have joined,          and the game has been started.          """ +          def startup_event_check(reaction_: Reaction, user_: Member): -            """ -            Make sure that this reaction is what we want to operate on -            """ +            """Make sure that this reaction is what we want to operate on.""" +              return (                  all((                      reaction_.message.id == startup.id,       # Reaction is on startup message @@ -471,6 +488,13 @@ class SnakeAndLaddersGame:                  self.avatar_images[user.id] = im      async def player_join(self, user: Member): +        """ +        Handle players joining the game. + +        Prevent player joining if they have already joined, if the game is full, or if the game is +        in a waiting state. +        """ +          for p in self.players:              if user == p:                  await self.channel.send(user.mention + " You are already in the game.", delete_after=10) @@ -491,6 +515,13 @@ class SnakeAndLaddersGame:          )      async def player_leave(self, user: Member): +        """ +        Handle players leaving the game. + +        Leaving is prevented if the user initiated the game or if they weren't part of it in the +        first place. +        """ +          if user == self.author:              await self.channel.send(                  user.mention + " You are the author, and cannot leave the game. Execute " @@ -515,6 +546,8 @@ class SnakeAndLaddersGame:          await self.channel.send(user.mention + " You are not in the match.", delete_after=10)      async def cancel_game(self, user: Member): +        """Allow the game author to cancel the running game.""" +          if not user == self.author:              await self.channel.send(user.mention + " Only the author of the game can cancel it.", delete_after=10)              return @@ -522,6 +555,13 @@ class SnakeAndLaddersGame:          self._destruct()      async def start_game(self, user: Member): +        """ +        Allow the game author to begin the game. + +        The game cannot be started if there aren't enough players joined or if the game is in a +        waiting state. +        """ +          if not user == self.author:              await self.channel.send(user.mention + " Only the author of the game can start it.", delete_after=10)              return @@ -540,10 +580,11 @@ class SnakeAndLaddersGame:          await self.start_round()      async def start_round(self): +        """Begin the round.""" +          def game_event_check(reaction_: Reaction, user_: Member): -            """ -            Make sure that this reaction is what we want to operate on -            """ +            """Make sure that this reaction is what we want to operate on.""" +              return (                  all((                      reaction_.message.id == self.positions.id,  # Reaction is on positions message @@ -634,6 +675,8 @@ class SnakeAndLaddersGame:          await self._complete_round()      async def player_roll(self, user: Member): +        """Handle the player's roll.""" +          if user.id not in self.player_tiles:              await self.channel.send(user.mention + " You are not in the match.", delete_after=10)              return diff --git a/bot/seasons/evergreen/uptime.py b/bot/seasons/evergreen/uptime.py index 1321da19..d6b59a69 100644 --- a/bot/seasons/evergreen/uptime.py +++ b/bot/seasons/evergreen/uptime.py @@ -10,18 +10,15 @@ log = logging.getLogger(__name__)  class Uptime: -    """ -    A cog for posting the bots uptime. -    """ +    """A cog for posting the bot's uptime."""      def __init__(self, bot):          self.bot = bot      @commands.command(name="uptime")      async def uptime(self, ctx): -        """ -        Returns the uptime of the bot. -        """ +        """Responds with the uptime of the bot.""" +          difference = relativedelta(start_time - arrow.utcnow())          uptime_string = start_time.shift(              seconds=-difference.seconds, @@ -32,7 +29,8 @@ class Uptime:          await ctx.send(f"I started up {uptime_string}.") -# Required in order to load the cog, use the class name in the add_cog function.  def setup(bot): +    """Uptime Cog load.""" +      bot.add_cog(Uptime(bot))      log.debug("Uptime cog loaded") | 
