diff options
-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") |