aboutsummaryrefslogtreecommitdiffstats
path: root/bot/seasons
diff options
context:
space:
mode:
Diffstat (limited to 'bot/seasons')
-rw-r--r--bot/seasons/easter/egg_decorating.py118
-rw-r--r--bot/seasons/easter/egghead_quiz.py121
-rw-r--r--bot/seasons/evergreen/lemonstats.py31
-rw-r--r--bot/seasons/halloween/spookyrating.py68
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")