diff options
Diffstat (limited to 'bot/seasons')
| -rw-r--r-- | bot/seasons/easter/egg_decorating.py | 118 | ||||
| -rw-r--r-- | bot/seasons/easter/egghead_quiz.py | 121 | ||||
| -rw-r--r-- | bot/seasons/evergreen/lemonstats.py | 31 | ||||
| -rw-r--r-- | bot/seasons/halloween/spookyrating.py | 68 |
4 files changed, 307 insertions, 31 deletions
diff --git a/bot/seasons/easter/egg_decorating.py b/bot/seasons/easter/egg_decorating.py new file mode 100644 index 00000000..b5f3e428 --- /dev/null +++ b/bot/seasons/easter/egg_decorating.py @@ -0,0 +1,118 @@ +import json +import logging +import random +from contextlib import suppress +from io import BytesIO +from pathlib import Path +from typing import Union + +import discord +from PIL import Image +from discord.ext import commands + +log = logging.getLogger(__name__) + +with open(Path("bot", "resources", "evergreen", "html_colours.json")) as f: + HTML_COLOURS = json.load(f) + +with open(Path("bot", "resources", "evergreen", "xkcd_colours.json")) as f: + XKCD_COLOURS = json.load(f) + +COLOURS = [ + (255, 0, 0, 255), (255, 128, 0, 255), (255, 255, 0, 255), (0, 255, 0, 255), + (0, 255, 255, 255), (0, 0, 255, 255), (255, 0, 255, 255), (128, 0, 128, 255) +] # Colours to be replaced - Red, Orange, Yellow, Green, Light Blue, Dark Blue, Pink, Purple + +IRREPLACEABLE = [ + (0, 0, 0, 0), (0, 0, 0, 255) +] # Colours that are meant to stay the same - Transparent and Black + + +class EggDecorating(commands.Cog): + """Decorate some easter eggs!""" + + def __init__(self, bot): + self.bot = bot + + @staticmethod + def replace_invalid(colour: str): + """Attempts to match with HTML or XKCD colour names, returning the int value.""" + with suppress(KeyError): + return int(HTML_COLOURS[colour], 16) + with suppress(KeyError): + return int(XKCD_COLOURS[colour], 16) + return None + + @commands.command(aliases=["decorateegg"]) + async def eggdecorate(self, ctx, *colours: Union[discord.Colour, str]): + """ + Picks a random egg design and decorates it using the given colours. + + Colours are split by spaces, unless you wrap the colour name in double quotes. + Discord colour names, HTML colour names, XKCD colour names and hex values are accepted. + """ + + if len(colours) < 2: + return await ctx.send("You must include at least 2 colours!") + + invalid = [] + colours = list(colours) + for idx, colour in enumerate(colours): + if isinstance(colour, discord.Colour): + continue + value = self.replace_invalid(colour) + if value: + colours[idx] = discord.Colour(value) + else: + invalid.append(colour) + + if len(invalid) > 1: + return await ctx.send(f"Sorry, I don't know these colours: {' '.join(invalid)}") + elif len(invalid) == 1: + return await ctx.send(f"Sorry, I don't know the colour {invalid[0]}!") + + async with ctx.typing(): + # expand list to 8 colours + colours_n = len(colours) + if colours_n < 8: + q, r = divmod(8, colours_n) + colours = colours * q + colours[:r] + num = random.randint(1, 6) + im = Image.open(Path("bot", "resources", "easter", "easter_eggs", f"design{num}.png")) + data = list(im.getdata()) + + replaceable = {x for x in data if x not in IRREPLACEABLE} + replaceable = sorted(replaceable, key=COLOURS.index) + + replacing_colours = {colour: colours[i] for i, colour in enumerate(replaceable)} + new_data = [] + for x in data: + if x in replacing_colours: + new_data.append((*replacing_colours[x].to_rgb(), 255)) + # Also ensures that the alpha channel has a value + else: + new_data.append(x) + new_im = Image.new(im.mode, im.size) + new_im.putdata(new_data) + + bufferedio = BytesIO() + new_im.save(bufferedio, format="PNG") + + bufferedio.seek(0) + + file = discord.File(bufferedio, filename="egg.png") # Creates file to be used in embed + embed = discord.Embed( + title="Your Colourful Easter Egg", + description="Here is your pretty little egg. Hope you like it!" + ) + embed.set_image(url="attachment://egg.png") + embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url) + + await ctx.send(file=file, embed=embed) + + +def setup(bot): + """Cog load.""" + + bot.add_cog(EggDecorating(bot)) + log.info("EggDecorating cog loaded.") diff --git a/bot/seasons/easter/egghead_quiz.py b/bot/seasons/easter/egghead_quiz.py new file mode 100644 index 00000000..8dd2c21d --- /dev/null +++ b/bot/seasons/easter/egghead_quiz.py @@ -0,0 +1,121 @@ +import asyncio +import logging +import random +from json import load +from pathlib import Path + +import discord +from discord.ext import commands + +from bot.constants import Colours + +log = logging.getLogger(__name__) + +with open(Path('bot', 'resources', 'easter', 'egghead_questions.json'), 'r', encoding="utf8") as f: + EGGHEAD_QUESTIONS = load(f) + + +EMOJIS = [ + '\U0001f1e6', '\U0001f1e7', '\U0001f1e8', '\U0001f1e9', '\U0001f1ea', + '\U0001f1eb', '\U0001f1ec', '\U0001f1ed', '\U0001f1ee', '\U0001f1ef', + '\U0001f1f0', '\U0001f1f1', '\U0001f1f2', '\U0001f1f3', '\U0001f1f4', + '\U0001f1f5', '\U0001f1f6', '\U0001f1f7', '\U0001f1f8', '\U0001f1f9', + '\U0001f1fa', '\U0001f1fb', '\U0001f1fc', '\U0001f1fd', '\U0001f1fe', + '\U0001f1ff' +] # Regional Indicators A-Z (used for voting) + +TIMELIMIT = 30 + + +class EggheadQuiz(commands.Cog): + """This cog contains the command for the Easter quiz!""" + + def __init__(self, bot): + self.bot = bot + self.quiz_messages = {} + + @commands.command(aliases=["eggheadquiz", "easterquiz"]) + async def eggquiz(self, ctx): + """ + Gives a random quiz question, waits 30 seconds and then outputs the answer + + Also informs of the percentages and votes of each option + """ + + random_question = random.choice(EGGHEAD_QUESTIONS) + question, answers = random_question["question"], random_question["answers"] + answers = [(EMOJIS[i], a) for i, a in enumerate(answers)] + correct = EMOJIS[random_question["correct_answer"]] + + valid_emojis = [emoji for emoji, _ in answers] + + description = f"You have {TIMELIMIT} seconds to vote.\n\n" + description += "\n".join([f"{emoji} -> **{answer}**" for emoji, answer in answers]) + + q_embed = discord.Embed(title=question, description=description, colour=Colours.pink) + + msg = await ctx.send(embed=q_embed) + for emoji in valid_emojis: + await msg.add_reaction(emoji) + + self.quiz_messages[msg.id] = valid_emojis + + await asyncio.sleep(TIMELIMIT) + + del self.quiz_messages[msg.id] + + msg = await ctx.channel.fetch_message(msg.id) # Refreshes message + + total_no = sum([len(await r.users().flatten()) for r in msg.reactions]) - len(valid_emojis) # - bot's reactions + + if total_no == 0: + return await msg.delete() # to avoid ZeroDivisionError if nobody reacts + + results = ["**VOTES:**"] + for emoji, _ in answers: + num = [len(await r.users().flatten()) for r in msg.reactions if str(r.emoji) == emoji][0] - 1 + percent = round(100 * num / total_no) + s = "" if num == 1 else "s" + string = f"{emoji} - {num} vote{s} ({percent}%)" + results.append(string) + + mentions = " ".join([ + u.mention for u in [ + await r.users().flatten() for r in msg.reactions if str(r.emoji) == correct + ][0] if not u.bot + ]) + + content = f"Well done {mentions} for getting it correct!" if mentions else "Nobody got it right..." + + a_embed = discord.Embed( + title=f"The correct answer was {correct}!", + description="\n".join(results), + colour=Colours.pink + ) + + await ctx.send(content, embed=a_embed) + + @staticmethod + async def already_reacted(message, user): + """Returns whether a given user has reacted more than once to a given message""" + users = [u.id for reaction in [await r.users().flatten() for r in message.reactions] for u in reaction] + return users.count(user.id) > 1 # Old reaction plus new reaction + + @commands.Cog.listener() + async def on_reaction_add(self, reaction, user): + """Listener to listen specifically for reactions of quiz messages""" + if user.bot: + return + if reaction.message.id not in self.quiz_messages: + return + if str(reaction.emoji) not in self.quiz_messages[reaction.message.id]: + return await reaction.message.remove_reaction(reaction, user) + if await self.already_reacted(reaction.message, user): + return await reaction.message.remove_reaction(reaction, user) + + +def setup(bot): + """Cog load.""" + + bot.add_cog(EggheadQuiz(bot)) + log.info("EggheadQuiz bot loaded") diff --git a/bot/seasons/evergreen/lemonstats.py b/bot/seasons/evergreen/lemonstats.py deleted file mode 100644 index b23c65a4..00000000 --- a/bot/seasons/evergreen/lemonstats.py +++ /dev/null @@ -1,31 +0,0 @@ -import logging - -from discord.ext import commands - - -log = logging.getLogger(__name__) - - -class LemonStats(commands.Cog): - """A cog for generating useful lemon-related statistics.""" - - def __init__(self, bot): - self.bot = bot - - @commands.command() - async def lemoncount(self, ctx: commands.Context): - """Count the number of users on the server with `'lemon'` in their nickname.""" - - async with ctx.typing(): - lemoncount = sum( - ['lemon' in server_member.display_name.lower() for server_member in self.bot.guilds[0].members] - ) - - await ctx.send(f"There are currently {lemoncount} lemons on the server.") - - -def setup(bot): - """Load LemonStats Cog.""" - - bot.add_cog(LemonStats(bot)) - log.info("LemonStats cog loaded") diff --git a/bot/seasons/halloween/spookyrating.py b/bot/seasons/halloween/spookyrating.py new file mode 100644 index 00000000..a9cfda9b --- /dev/null +++ b/bot/seasons/halloween/spookyrating.py @@ -0,0 +1,68 @@ +import bisect +import json +import logging +import random +from pathlib import Path + +import discord +from discord.ext import commands + +from bot.constants import Colours + +log = logging.getLogger(__name__) + +with Path('bot', 'resources', 'halloween', 'spooky_rating.json').open() as file: + SPOOKY_DATA = json.load(file) + SPOOKY_DATA = sorted((int(key), value) for key, value in SPOOKY_DATA.items()) + + +class SpookyRating(commands.Cog): + """A cog for calculating one's spooky rating""" + + def __init__(self, bot): + self.bot = bot + self.local_random = random.Random() + + @commands.command() + @commands.cooldown(rate=1, per=5, type=commands.BucketType.user) + async def spookyrating(self, ctx, who: discord.Member = None): + """ + Calculates the spooky rating of someone. + + Any user will always yield the same result, no matter who calls the command + """ + + if who is None: + who = ctx.author + + # This ensures that the same result over multiple runtimes + self.local_random.seed(who.id) + spooky_percent = self.local_random.randint(1, 101) + + # We need the -1 due to how bisect returns the point + # see the documentation for further detail + # https://docs.python.org/3/library/bisect.html#bisect.bisect + index = bisect.bisect(SPOOKY_DATA, (spooky_percent,)) - 1 + + _, data = SPOOKY_DATA[index] + + embed = discord.Embed( + title=data['title'], + description=f'{who} scored {spooky_percent}%!', + color=Colours.orange + ) + embed.add_field( + name='A whisper from Satan', + value=data['text'] + ) + embed.set_thumbnail( + url=data['image'] + ) + + await ctx.send(embed=embed) + + +def setup(bot): + """Cog load.""" + bot.add_cog(SpookyRating(bot)) + log.info("SpookyRating cog loaded") |