aboutsummaryrefslogtreecommitdiffstats
path: root/bot
diff options
context:
space:
mode:
authorGravatar Vivaan Parashar <[email protected]>2021-06-02 16:21:04 +0530
committerGravatar Vivaan Parashar <[email protected]>2021-06-02 16:21:04 +0530
commitbd978b11521d7fd2957d3f8541820029acd26cb5 (patch)
treeb3832bedbd2430789a5f1e4c64b64efb8806dc0d /bot
parentUpdate branch (diff)
parentAdded "centisecond" in trivia_quiz.json (#751) (diff)
Merge branch 'main' of https://github.com/python-discord/sir-lancebot into feature/stackoverflow
Diffstat (limited to 'bot')
-rw-r--r--bot/__init__.py7
-rw-r--r--bot/constants.py30
-rw-r--r--bot/exts/christmas/advent_of_code/_cog.py7
-rw-r--r--bot/exts/christmas/advent_of_code/_helpers.py22
-rw-r--r--bot/exts/evergreen/bookmark.py8
-rw-r--r--bot/exts/evergreen/error_handler.py41
-rw-r--r--bot/exts/evergreen/source.py11
-rw-r--r--bot/exts/evergreen/status_codes.py23
-rw-r--r--bot/exts/evergreen/trivia_quiz.py446
-rw-r--r--bot/resources/evergreen/trivia_quiz.json335
-rw-r--r--bot/utils/pagination.py19
11 files changed, 823 insertions, 126 deletions
diff --git a/bot/__init__.py b/bot/__init__.py
index e5ed9d92..85ae4758 100644
--- a/bot/__init__.py
+++ b/bot/__init__.py
@@ -1,3 +1,10 @@
+try:
+ from dotenv import load_dotenv
+ print("Found .env file, loading environment variables from it.")
+ load_dotenv(override=True)
+except ModuleNotFoundError:
+ pass
+
import asyncio
import logging
import logging.handlers
diff --git a/bot/constants.py b/bot/constants.py
index 37be84b6..aa26be83 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -22,6 +22,7 @@ __all__ = (
"Wolfram",
"Reddit",
"RedisConfig",
+ "RedirectOutput",
"MODERATION_ROLES",
"STAFF_ROLES",
"WHITELISTED_CHANNELS",
@@ -140,19 +141,20 @@ class Client(NamedTuple):
class Colours:
- blue = 0x0279fd
- bright_green = 0x01d277
- dark_green = 0x1f8b4c
- orange = 0xe67e22
- pink = 0xcf84e0
- purple = 0xb734eb
- soft_green = 0x68c290
- soft_orange = 0xf9cb54
- soft_red = 0xcd6d6d
- yellow = 0xf9f586
+ blue = 0x0279FD
+ bright_green = 0x01D277
+ dark_green = 0x1F8B4C
+ orange = 0xE67E22
+ pink = 0xCF84E0
+ purple = 0xB734EB
+ soft_green = 0x68C290
+ soft_orange = 0xF9CB54
+ soft_red = 0xCD6D6D
+ yellow = 0xF9F586
python_blue = 0x4B8BBE
python_yellow = 0xFFD43B
- grass_green = 0x66ff00
+ grass_green = 0x66FF00
+ gold = 0xE6C200
easter_like_colours = [
(255, 247, 0),
@@ -179,7 +181,7 @@ class Emojis:
envelope = "\U0001F4E8"
trashcan = environ.get("TRASHCAN_EMOJI", "<:trashcan:637136429717389331>")
ok_hand = ":ok_hand:"
- hand_raised = "\U0001f64b"
+ hand_raised = "\U0001F64B"
dice_1 = "<:dice_1:755891608859443290>"
dice_2 = "<:dice_2:755891608741740635>"
@@ -310,6 +312,10 @@ class Source:
github_avatar_url = "https://avatars1.githubusercontent.com/u/9919"
+class RedirectOutput:
+ delete_delay: int = 10
+
+
class Reddit:
subreddits = ["r/Python"]
diff --git a/bot/exts/christmas/advent_of_code/_cog.py b/bot/exts/christmas/advent_of_code/_cog.py
index ead84544..3d61753b 100644
--- a/bot/exts/christmas/advent_of_code/_cog.py
+++ b/bot/exts/christmas/advent_of_code/_cog.py
@@ -3,6 +3,7 @@ import logging
from datetime import datetime, timedelta
from pathlib import Path
+import arrow
import discord
from discord.ext import commands
@@ -100,11 +101,11 @@ class AdventOfCode(commands.Cog):
async def aoc_countdown(self, ctx: commands.Context) -> None:
"""Return time left until next day."""
if not _helpers.is_in_advent():
- datetime_now = datetime.now(_helpers.EST)
+ datetime_now = arrow.now(_helpers.EST)
# Calculate the delta to this & next year's December 1st to see which one is closest and not in the past
- this_year = datetime(datetime_now.year, 12, 1, tzinfo=_helpers.EST)
- next_year = datetime(datetime_now.year + 1, 12, 1, tzinfo=_helpers.EST)
+ this_year = arrow.get(datetime(datetime_now.year, 12, 1), _helpers.EST)
+ next_year = arrow.get(datetime(datetime_now.year + 1, 12, 1), _helpers.EST)
deltas = (dec_first - datetime_now for dec_first in (this_year, next_year))
delta = min(delta for delta in deltas if delta >= timedelta()) # timedelta() gives 0 duration delta
diff --git a/bot/exts/christmas/advent_of_code/_helpers.py b/bot/exts/christmas/advent_of_code/_helpers.py
index f4a258c0..96de90c4 100644
--- a/bot/exts/christmas/advent_of_code/_helpers.py
+++ b/bot/exts/christmas/advent_of_code/_helpers.py
@@ -9,8 +9,8 @@ import typing
from typing import Tuple
import aiohttp
+import arrow
import discord
-import pytz
from bot.bot import Bot
from bot.constants import AdventOfCode, Channels, Colours
@@ -48,7 +48,7 @@ AOC_EMBED_THUMBNAIL = (
)
# Create an easy constant for the EST timezone
-EST = pytz.timezone("EST")
+EST = "America/New_York"
# Step size for the challenge countdown status
COUNTDOWN_STEP = 60 * 5
@@ -395,13 +395,13 @@ def is_in_advent() -> bool:
something for the next Advent of Code challenge should run. As the puzzle
published on the 25th is the last puzzle, this check excludes that date.
"""
- return datetime.datetime.now(EST).day in range(1, 25) and datetime.datetime.now(EST).month == 12
+ return arrow.now(EST).day in range(1, 25) and arrow.now(EST).month == 12
def time_left_to_est_midnight() -> Tuple[datetime.datetime, datetime.timedelta]:
"""Calculate the amount of time left until midnight EST/UTC-5."""
# Change all time properties back to 00:00
- todays_midnight = datetime.datetime.now(EST).replace(
+ todays_midnight = arrow.now(EST).replace(
microsecond=0,
second=0,
minute=0,
@@ -412,7 +412,7 @@ def time_left_to_est_midnight() -> Tuple[datetime.datetime, datetime.timedelta]:
tomorrow = todays_midnight + datetime.timedelta(days=1)
# Calculate the timedelta between the current time and midnight
- return tomorrow, tomorrow - datetime.datetime.now(EST)
+ return tomorrow, tomorrow - arrow.now(EST)
async def wait_for_advent_of_code(*, hours_before: int = 1) -> None:
@@ -430,9 +430,9 @@ async def wait_for_advent_of_code(*, hours_before: int = 1) -> None:
if we're already past the Advent of Code edition the bot is currently
configured for.
"""
- start = datetime.datetime(AdventOfCode.year, 12, 1, 0, 0, 0, tzinfo=EST)
+ start = arrow.get(datetime.datetime(AdventOfCode.year, 12, 1), EST)
target = start - datetime.timedelta(hours=hours_before)
- now = datetime.datetime.now(EST)
+ now = arrow.now(EST)
# If we've already reached or passed to target, we
# simply return immediately.
@@ -474,10 +474,10 @@ async def countdown_status(bot: Bot) -> None:
# sleeping for the entire year, it will only wait in the currently
# configured year. This means that the task will only start hibernating once
# we start preparing the next event by changing environment variables.
- last_challenge = datetime.datetime(AdventOfCode.year, 12, 25, 0, 0, 0, tzinfo=EST)
+ last_challenge = arrow.get(datetime.datetime(AdventOfCode.year, 12, 25), EST)
end = last_challenge + datetime.timedelta(hours=1)
- while datetime.datetime.now(EST) < end:
+ while arrow.now(EST) < end:
_, time_left = time_left_to_est_midnight()
aligned_seconds = int(math.ceil(time_left.seconds / COUNTDOWN_STEP)) * COUNTDOWN_STEP
@@ -534,8 +534,8 @@ async def new_puzzle_notification(bot: Bot) -> None:
# The last event day is 25 December, so we only have to schedule
# a reminder if the current day is before 25 December.
- end = datetime.datetime(AdventOfCode.year, 12, 25, tzinfo=EST)
- while datetime.datetime.now(EST) < end:
+ end = arrow.get(datetime.datetime(AdventOfCode.year, 12, 25), EST)
+ while arrow.now(EST) < end:
log.trace("Started puzzle notification loop.")
tomorrow, time_left = time_left_to_est_midnight()
diff --git a/bot/exts/evergreen/bookmark.py b/bot/exts/evergreen/bookmark.py
index 29915627..85c9b46f 100644
--- a/bot/exts/evergreen/bookmark.py
+++ b/bot/exts/evergreen/bookmark.py
@@ -1,6 +1,7 @@
import asyncio
import logging
import random
+import typing as t
import discord
from discord.ext import commands
@@ -88,11 +89,16 @@ class Bookmark(commands.Cog):
async def bookmark(
self,
ctx: commands.Context,
- target_message: WrappedMessageConverter,
+ target_message: t.Optional[WrappedMessageConverter],
*,
title: str = "Bookmark"
) -> None:
"""Send the author a link to `target_message` via DMs."""
+ if not target_message:
+ if not ctx.message.reference:
+ raise commands.UserInputError("You must either provide a valid message to bookmark, or reply to one.")
+ target_message = ctx.message.reference.resolved
+
# Prevent users from bookmarking a message in a channel they don't have access to
permissions = ctx.author.permissions_in(target_message.channel)
if not permissions.read_messages:
diff --git a/bot/exts/evergreen/error_handler.py b/bot/exts/evergreen/error_handler.py
index de8e53d0..5873fb83 100644
--- a/bot/exts/evergreen/error_handler.py
+++ b/bot/exts/evergreen/error_handler.py
@@ -1,3 +1,4 @@
+import difflib
import logging
import math
import random
@@ -8,16 +9,22 @@ from discord.ext import commands
from sentry_sdk import push_scope
from bot.bot import Bot
-from bot.constants import Channels, Colours, ERROR_REPLIES, NEGATIVE_REPLIES
+from bot.constants import Channels, Colours, ERROR_REPLIES, NEGATIVE_REPLIES, RedirectOutput
from bot.utils.decorators import InChannelCheckFailure, InMonthCheckFailure
from bot.utils.exceptions import UserNotPlayingError
log = logging.getLogger(__name__)
+QUESTION_MARK_ICON = "https://cdn.discordapp.com/emojis/512367613339369475.png"
+
+
class CommandErrorHandler(commands.Cog):
"""A error handler for the PythonDiscord server."""
+ def __init__(self, bot: Bot) -> None:
+ self.bot = bot
+
@staticmethod
def revert_cooldown_counter(command: commands.Command, message: Message) -> None:
"""Undoes the last cooldown counter for user-error cases."""
@@ -58,6 +65,7 @@ class CommandErrorHandler(commands.Cog):
)
if isinstance(error, commands.CommandNotFound):
+ await self.send_command_suggestion(ctx, ctx.invoked_with)
return
if isinstance(error, (InChannelCheckFailure, InMonthCheckFailure)):
@@ -129,7 +137,36 @@ class CommandErrorHandler(commands.Cog):
log.exception(f"Unhandled command error: {str(error)}", exc_info=error)
+ async def send_command_suggestion(self, ctx: commands.Context, command_name: str) -> None:
+ """Sends user similar commands if any can be found."""
+ raw_commands = []
+ for cmd in self.bot.walk_commands():
+ if not cmd.hidden:
+ raw_commands += (cmd.name, *cmd.aliases)
+ if similar_command_data := difflib.get_close_matches(command_name, raw_commands, 1):
+ similar_command_name = similar_command_data[0]
+ similar_command = self.bot.get_command(similar_command_name)
+
+ if not similar_command:
+ return
+
+ log_msg = "Cancelling attempt to suggest a command due to failed checks."
+ try:
+ if not await similar_command.can_run(ctx):
+ log.debug(log_msg)
+ return
+ except commands.errors.CommandError as cmd_error:
+ log.debug(log_msg)
+ await self.on_command_error(ctx, cmd_error)
+ return
+
+ misspelled_content = ctx.message.content
+ e = Embed()
+ e.set_author(name="Did you mean:", icon_url=QUESTION_MARK_ICON)
+ e.description = misspelled_content.replace(command_name, similar_command_name, 1)
+ await ctx.send(embed=e, delete_after=RedirectOutput.delete_delay)
+
def setup(bot: Bot) -> None:
"""Load the ErrorHandler cog."""
- bot.add_cog(CommandErrorHandler())
+ bot.add_cog(CommandErrorHandler(bot))
diff --git a/bot/exts/evergreen/source.py b/bot/exts/evergreen/source.py
index 8fb72143..fc209bc3 100644
--- a/bot/exts/evergreen/source.py
+++ b/bot/exts/evergreen/source.py
@@ -33,7 +33,8 @@ class BotSource(commands.Cog):
Raise BadArgument if `source_item` is a dynamically-created object (e.g. via internal eval).
"""
if isinstance(source_item, commands.Command):
- src = source_item.callback.__code__
+ callback = inspect.unwrap(source_item.callback)
+ src = callback.__code__
filename = src.co_filename
else:
src = type(source_item)
@@ -64,12 +65,8 @@ class BotSource(commands.Cog):
url, location, first_line = self.get_source_link(source_object)
if isinstance(source_object, commands.Command):
- if source_object.cog_name == "Help":
- title = "Help Command"
- description = source_object.__doc__.splitlines()[1]
- else:
- description = source_object.short_doc
- title = f"Command: {source_object.qualified_name}"
+ description = source_object.short_doc
+ title = f"Command: {source_object.qualified_name}"
else:
title = f"Cog: {source_object.qualified_name}"
description = source_object.description.splitlines()[0]
diff --git a/bot/exts/evergreen/status_codes.py b/bot/exts/evergreen/status_codes.py
index a866692e..181c71ce 100644
--- a/bot/exts/evergreen/status_codes.py
+++ b/bot/exts/evergreen/status_codes.py
@@ -1,26 +1,30 @@
from http import HTTPStatus
+from random import choice
import discord
from discord.ext import commands
from bot.bot import Bot
-from bot.utils.extensions import invoke_help_command
HTTP_DOG_URL = "https://httpstatusdogs.com/img/{code}.jpg"
HTTP_CAT_URL = "https://http.cat/{code}.jpg"
class HTTPStatusCodes(commands.Cog):
- """Commands that give HTTP statuses described and visualized by cats and dogs."""
+ """
+ Fetch an image depicting HTTP status codes as a dog or a cat.
+
+ If neither animal is selected a cat or dog is chosen randomly for the given status code.
+ """
def __init__(self, bot: Bot):
self.bot = bot
- @commands.group(name="http_status", aliases=("status", "httpstatus"))
- async def http_status_group(self, ctx: commands.Context) -> None:
- """Group containing dog and cat http status code commands."""
- if not ctx.invoked_subcommand:
- await invoke_help_command(ctx)
+ @commands.group(name="http_status", aliases=("status", "httpstatus"), invoke_without_command=True)
+ async def http_status_group(self, ctx: commands.Context, code: int) -> None:
+ """Choose a cat or dog randomly for the given status code."""
+ subcmd = choice((self.http_cat, self.http_dog))
+ await subcmd(ctx, code)
@http_status_group.command(name="cat")
async def http_cat(self, ctx: commands.Context, code: int) -> None:
@@ -48,6 +52,11 @@ class HTTPStatusCodes(commands.Cog):
@http_status_group.command(name="dog")
async def http_dog(self, ctx: commands.Context, code: int) -> None:
"""Sends an embed with an image of a dog, portraying the status code."""
+ # These codes aren't server-friendly.
+ if code in (304, 422):
+ await self.http_cat(ctx, code)
+ return
+
embed = discord.Embed(title=f"**Status: {code}**")
url = HTTP_DOG_URL.format(code=code)
diff --git a/bot/exts/evergreen/trivia_quiz.py b/bot/exts/evergreen/trivia_quiz.py
index 352d5ae8..a8d10afd 100644
--- a/bot/exts/evergreen/trivia_quiz.py
+++ b/bot/exts/evergreen/trivia_quiz.py
@@ -1,55 +1,235 @@
import asyncio
import json
import logging
+import operator
import random
+from dataclasses import dataclass
from pathlib import Path
+from typing import Callable, List, Optional
import discord
from discord.ext import commands
from fuzzywuzzy import fuzz
from bot.bot import Bot
-from bot.constants import Roles
-
+from bot.constants import Colours, NEGATIVE_REPLIES, Roles
logger = logging.getLogger(__name__)
+DEFAULT_QUESTION_LIMIT = 6
+STANDARD_VARIATION_TOLERANCE = 83
+DYNAMICALLY_GEN_VARIATION_TOLERANCE = 95
WRONG_ANS_RESPONSE = [
"No one answered correctly!",
- "Better luck next time"
+ "Better luck next time...",
+]
+
+N_PREFIX_STARTS_AT = 5
+N_PREFIXES = [
+ "penta", "hexa", "hepta", "octa", "nona",
+ "deca", "hendeca", "dodeca", "trideca", "tetradeca",
+]
+
+PLANETS = [
+ ("1st", "Mercury"),
+ ("2nd", "Venus"),
+ ("3rd", "Earth"),
+ ("4th", "Mars"),
+ ("5th", "Jupiter"),
+ ("6th", "Saturn"),
+ ("7th", "Uranus"),
+ ("8th", "Neptune"),
+]
+
+TAXONOMIC_HIERARCHY = [
+ "species", "genus", "family", "order",
+ "class", "phylum", "kingdom", "domain",
]
+UNITS_TO_BASE_UNITS = {
+ "hertz": ("(unit of frequency)", "s^-1"),
+ "newton": ("(unit of force)", "m*kg*s^-2"),
+ "pascal": ("(unit of pressure & stress)", "m^-1*kg*s^-2"),
+ "joule": ("(unit of energy & quantity of heat)", "m^2*kg*s^-2"),
+ "watt": ("(unit of power)", "m^2*kg*s^-3"),
+ "coulomb": ("(unit of electric charge & quantity of electricity)", "s*A"),
+ "volt": ("(unit of voltage & electromotive force)", "m^2*kg*s^-3*A^-1"),
+ "farad": ("(unit of capacitance)", "m^-2*kg^-1*s^4*A^2"),
+ "ohm": ("(unit of electric resistance)", "m^2*kg*s^-3*A^-2"),
+ "weber": ("(unit of magnetic flux)", "m^2*kg*s^-2*A^-1"),
+ "tesla": ("(unit of magnetic flux density)", "kg*s^-2*A^-1"),
+}
+
+
+@dataclass(frozen=True)
+class QuizEntry:
+ """Dataclass for a quiz entry (a question and a string containing answers separated by commas)."""
+
+ question: str
+ answer: str
+
+
+def linear_system(q_format: str, a_format: str) -> QuizEntry:
+ """Generate a system of linear equations with two unknowns."""
+ x, y = random.randint(2, 5), random.randint(2, 5)
+ answer = a_format.format(x, y)
+
+ coeffs = random.sample(range(1, 6), 4)
+
+ question = q_format.format(
+ coeffs[0],
+ coeffs[1],
+ coeffs[0] * x + coeffs[1] * y,
+ coeffs[2],
+ coeffs[3],
+ coeffs[2] * x + coeffs[3] * y,
+ )
+
+ return QuizEntry(question, answer)
+
+
+def mod_arith(q_format: str, a_format: str) -> QuizEntry:
+ """Generate a basic modular arithmetic question."""
+ quotient, m, b = random.randint(30, 40), random.randint(10, 20), random.randint(200, 350)
+ ans = random.randint(0, 9) # max remainder is 9, since the minimum modulus is 10
+ a = quotient * m + ans - b
+
+ question = q_format.format(a, b, m)
+ answer = a_format.format(ans)
+
+ return QuizEntry(question, answer)
+
+
+def ngonal_prism(q_format: str, a_format: str) -> QuizEntry:
+ """Generate a question regarding vertices on n-gonal prisms."""
+ n = random.randint(0, len(N_PREFIXES) - 1)
+
+ question = q_format.format(N_PREFIXES[n])
+ answer = a_format.format((n + N_PREFIX_STARTS_AT) * 2)
+
+ return QuizEntry(question, answer)
+
+
+def imag_sqrt(q_format: str, a_format: str) -> QuizEntry:
+ """Generate a negative square root question."""
+ ans_coeff = random.randint(3, 10)
+
+ question = q_format.format(ans_coeff ** 2)
+ answer = a_format.format(ans_coeff)
+
+ return QuizEntry(question, answer)
+
+
+def binary_calc(q_format: str, a_format: str) -> QuizEntry:
+ """Generate a binary calculation question."""
+ a = random.randint(15, 20)
+ b = random.randint(10, a)
+ oper = random.choice(
+ (
+ ("+", operator.add),
+ ("-", operator.sub),
+ ("*", operator.mul),
+ )
+ )
+
+ # if the operator is multiplication, lower the values of the two operands to make it easier
+ if oper[0] == "*":
+ a -= 5
+ b -= 5
+
+ question = q_format.format(a, oper[0], b)
+ answer = a_format.format(oper[1](a, b))
+
+ return QuizEntry(question, answer)
+
+
+def solar_system(q_format: str, a_format: str) -> QuizEntry:
+ """Generate a question on the planets of the Solar System."""
+ planet = random.choice(PLANETS)
+
+ question = q_format.format(planet[0])
+ answer = a_format.format(planet[1])
+
+ return QuizEntry(question, answer)
+
+
+def taxonomic_rank(q_format: str, a_format: str) -> QuizEntry:
+ """Generate a question on taxonomic classification."""
+ level = random.randint(0, len(TAXONOMIC_HIERARCHY) - 2)
+
+ question = q_format.format(TAXONOMIC_HIERARCHY[level])
+ answer = a_format.format(TAXONOMIC_HIERARCHY[level + 1])
+
+ return QuizEntry(question, answer)
+
+
+def base_units_convert(q_format: str, a_format: str) -> QuizEntry:
+ """Generate a SI base units conversion question."""
+ unit = random.choice(list(UNITS_TO_BASE_UNITS))
+
+ question = q_format.format(
+ unit + " " + UNITS_TO_BASE_UNITS[unit][0]
+ )
+ answer = a_format.format(
+ UNITS_TO_BASE_UNITS[unit][1]
+ )
+
+ return QuizEntry(question, answer)
+
+
+DYNAMIC_QUESTIONS_FORMAT_FUNCS = {
+ 201: linear_system,
+ 202: mod_arith,
+ 203: ngonal_prism,
+ 204: imag_sqrt,
+ 205: binary_calc,
+ 301: solar_system,
+ 302: taxonomic_rank,
+ 303: base_units_convert,
+}
+
class TriviaQuiz(commands.Cog):
"""A cog for all quiz commands."""
def __init__(self, bot: Bot) -> None:
self.bot = bot
- self.questions = self.load_questions()
+
self.game_status = {} # A variable to store the game status: either running or not running.
self.game_owners = {} # A variable to store the person's ID who started the quiz game in a channel.
- self.question_limit = 4
+
+ self.questions = self.load_questions()
+ self.question_limit = 0
+
self.player_scores = {} # A variable to store all player's scores for a bot session.
self.game_player_scores = {} # A variable to store temporary game player's scores.
+
self.categories = {
- "general": "Test your general knowledge"
- # "retro": "Questions related to retro gaming."
+ "general": "Test your general knowledge.",
+ "retro": "Questions related to retro gaming.",
+ "math": "General questions about mathematics ranging from grade 8 to grade 12.",
+ "science": "Put your understanding of science to the test!",
}
@staticmethod
def load_questions() -> dict:
"""Load the questions from the JSON file."""
p = Path("bot", "resources", "evergreen", "trivia_quiz.json")
- return json.loads(p.read_text("utf8"))
- @commands.group(name="quiz", aliases=("trivia",), invoke_without_command=True)
- async def quiz_game(self, ctx: commands.Context, category: str = None) -> None:
+ return json.loads(p.read_text(encoding="utf-8"))
+
+ @commands.group(name="quiz", aliases=["trivia"], invoke_without_command=True)
+ async def quiz_game(self, ctx: commands.Context, category: Optional[str], questions: Optional[int]) -> None:
"""
Start a quiz!
Questions for the quiz can be selected from the following categories:
- - general : Test your general knowledge. (default)
+ - general: Test your general knowledge. (default)
+ - retro: Questions related to retro gaming.
+ - math: General questions about mathematics ranging from grade 8 to grade 12.
+ - science: Put your understanding of science to the test!
+
(More to come!)
"""
if ctx.channel.id not in self.game_status:
@@ -59,9 +239,9 @@ class TriviaQuiz(commands.Cog):
self.game_player_scores[ctx.channel.id] = {}
# Stop game if running.
- if self.game_status[ctx.channel.id] is True:
+ if self.game_status[ctx.channel.id]:
await ctx.send(
- f"Game is already running..."
+ "Game is already running... "
f"do `{self.bot.command_prefix}quiz stop`"
)
return
@@ -76,8 +256,35 @@ class TriviaQuiz(commands.Cog):
await ctx.send(embed=embed)
return
+ topic = self.questions[category]
+ topic_length = len(topic)
+
+ if questions is None:
+ self.question_limit = DEFAULT_QUESTION_LIMIT
+ else:
+ if questions > topic_length:
+ await ctx.send(
+ embed=self.make_error_embed(
+ f"This category only has {topic_length} questions. "
+ "Please input a lower value!"
+ )
+ )
+ return
+
+ elif questions < 1:
+ await ctx.send(
+ embed=self.make_error_embed(
+ "You must choose to complete at least one question. "
+ f"(or enter nothing for the default value of {DEFAULT_QUESTION_LIMIT + 1} questions)"
+ )
+ )
+ return
+
+ else:
+ self.question_limit = questions - 1
+
# Start game if not running.
- if self.game_status[ctx.channel.id] is False:
+ if not self.game_status[ctx.channel.id]:
self.game_owners[ctx.channel.id] = ctx.author
self.game_status[ctx.channel.id] = True
start_embed = self.make_start_embed(category)
@@ -85,11 +292,10 @@ class TriviaQuiz(commands.Cog):
await ctx.send(embed=start_embed) # send an embed with the rules
await asyncio.sleep(1)
- topic = self.questions[category]
-
done_question = []
hint_no = 0
- answer = None
+ answers = None
+
while self.game_status[ctx.channel.id]:
# Exit quiz if number of questions for a round are already sent.
if len(done_question) > self.question_limit and hint_no == 0:
@@ -111,34 +317,58 @@ class TriviaQuiz(commands.Cog):
done_question.append(question_dict["id"])
break
- q = question_dict["question"]
- answer = question_dict["answer"]
+ if "dynamic_id" not in question_dict:
+ question = question_dict["question"]
+ answers = question_dict["answer"].split(", ")
+
+ var_tol = STANDARD_VARIATION_TOLERANCE
+ else:
+ format_func = DYNAMIC_QUESTIONS_FORMAT_FUNCS[question_dict["dynamic_id"]]
+
+ quiz_entry = format_func(
+ question_dict["question"],
+ question_dict["answer"],
+ )
- embed = discord.Embed(colour=discord.Colour.gold())
- embed.title = f"Question #{len(done_question)}"
- embed.description = q
- await ctx.send(embed=embed) # Send question embed.
+ question, answers = quiz_entry.question, quiz_entry.answer
+ answers = [answers]
- # A function to check whether user input is the correct answer(close to the right answer)
- def check(m: discord.Message) -> bool:
- return (
- m.channel == ctx.channel
- and fuzz.ratio(answer.lower(), m.content.lower()) > 85
+ var_tol = DYNAMICALLY_GEN_VARIATION_TOLERANCE
+
+ embed = discord.Embed(
+ colour=Colours.gold,
+ title=f"Question #{len(done_question)}",
+ description=question,
)
+ if img_url := question_dict.get("img_url"):
+ embed.set_image(url=img_url)
+
+ await ctx.send(embed=embed)
+
+ def check_func(variation_tolerance: int) -> Callable[[discord.Message], bool]:
+ def contains_correct_answer(m: discord.Message) -> bool:
+ return m.channel == ctx.channel and any(
+ fuzz.ratio(answer.lower(), m.content.lower()) > variation_tolerance
+ for answer in answers
+ )
+
+ return contains_correct_answer
+
try:
- msg = await self.bot.wait_for("message", check=check, timeout=10)
+ msg = await self.bot.wait_for("message", check=check_func(var_tol), timeout=10)
except asyncio.TimeoutError:
# In case of TimeoutError and the game has been stopped, then do nothing.
- if self.game_status[ctx.channel.id] is False:
+ if not self.game_status[ctx.channel.id]:
break
- # if number of hints sent or time alerts sent is less than 2, then send one.
if hint_no < 2:
hint_no += 1
+
if "hints" in question_dict:
hints = question_dict["hints"]
- await ctx.send(f"**Hint #{hint_no+1}\n**{hints[hint_no]}")
+
+ await ctx.send(f"**Hint #{hint_no}\n**{hints[hint_no - 1]}")
else:
await ctx.send(f"{30 - hint_no * 10}s left!")
@@ -151,10 +381,17 @@ class TriviaQuiz(commands.Cog):
response = random.choice(WRONG_ANS_RESPONSE)
await ctx.send(response)
- await self.send_answer(ctx.channel, question_dict)
+
+ await self.send_answer(
+ ctx.channel,
+ answers,
+ False,
+ question_dict,
+ self.question_limit - len(done_question) + 1,
+ )
await asyncio.sleep(1)
- hint_no = 0 # init hint_no = 0 so that 2 hints/time alerts can be sent for the new question.
+ hint_no = 0 # Reset the hint counter so that on the next round, it's in the initial state
await self.send_score(ctx.channel, self.game_player_scores[ctx.channel.id])
await asyncio.sleep(2)
@@ -162,8 +399,7 @@ class TriviaQuiz(commands.Cog):
if self.game_status[ctx.channel.id] is False:
break
- # Reduce points by 25 for every hint/time alert that has been sent.
- points = 100 - 25*hint_no
+ points = 100 - 25 * hint_no
if msg.author in self.game_player_scores[ctx.channel.id]:
self.game_player_scores[ctx.channel.id][msg.author] += points
else:
@@ -178,23 +414,50 @@ class TriviaQuiz(commands.Cog):
hint_no = 0
await ctx.send(f"{msg.author.mention} got the correct answer :tada: {points} points!")
- await self.send_answer(ctx.channel, question_dict)
+
+ await self.send_answer(
+ ctx.channel,
+ answers,
+ True,
+ question_dict,
+ self.question_limit - len(done_question) + 1,
+ )
await self.send_score(ctx.channel, self.game_player_scores[ctx.channel.id])
+
await asyncio.sleep(2)
- @staticmethod
- def make_start_embed(category: str) -> discord.Embed:
+ def make_start_embed(self, category: str) -> discord.Embed:
"""Generate a starting/introduction embed for the quiz."""
- start_embed = discord.Embed(colour=discord.Colour.red())
- start_embed.title = "Quiz game Starting!!"
- start_embed.description = "Each game consists of 5 questions.\n"
- start_embed.description += "**Rules :**\nNo cheating and have fun!"
- start_embed.description += f"\n **Category** : {category}"
+ start_embed = discord.Embed(
+ colour=Colours.blue,
+ title="Quiz game starting!",
+ description=(
+ f"This game consists of {self.question_limit + 1} questions.\n"
+ "**Rules: **No cheating and have fun!\n"
+ f"**Category**: {category}"
+ ),
+ )
+
start_embed.set_footer(
- text="Points for each question reduces by 25 after 10s or after a hint. Total time is 30s per question"
+ text=(
+ "Points for each question reduces by 25 after 10s or after a hint. "
+ "Total time is 30s per question"
+ )
)
+
return start_embed
+ @staticmethod
+ def make_error_embed(desc: str) -> discord.Embed:
+ """Generate an error embed with the given description."""
+ error_embed = discord.Embed(
+ colour=Colours.soft_red,
+ title=random.choice(NEGATIVE_REPLIES),
+ description=desc,
+ )
+
+ return error_embed
+
@quiz_game.command(name="stop")
async def stop_quiz(self, ctx: commands.Context) -> None:
"""
@@ -202,21 +465,24 @@ class TriviaQuiz(commands.Cog):
Note: Only mods or the owner of the quiz can stop it.
"""
- if self.game_status[ctx.channel.id] is True:
- # Check if the author is the game starter or a moderator.
- if (
- ctx.author == self.game_owners[ctx.channel.id]
- or any(Roles.moderator == role.id for role in ctx.author.roles)
- ):
- await ctx.send("Quiz stopped.")
- await self.declare_winner(ctx.channel, self.game_player_scores[ctx.channel.id])
+ try:
+ if self.game_status[ctx.channel.id]:
+ # Check if the author is the game starter or a moderator.
+ if ctx.author == self.game_owners[ctx.channel.id] or any(
+ Roles.moderator == role.id for role in ctx.author.roles
+ ):
+ self.game_status[ctx.channel.id] = False
+ del self.game_owners[ctx.channel.id]
+ self.game_player_scores[ctx.channel.id] = {}
+
+ await ctx.send("Quiz stopped.")
+ await self.declare_winner(ctx.channel, self.game_player_scores[ctx.channel.id])
- self.game_status[ctx.channel.id] = False
- del self.game_owners[ctx.channel.id]
- self.game_player_scores[ctx.channel.id] = {}
+ else:
+ await ctx.send(f"{ctx.author.mention}, you are not authorised to stop this game :ghost:!")
else:
- await ctx.send(f"{ctx.author.mention}, you are not authorised to stop this game :ghost:!")
- else:
+ await ctx.send("No quiz running.")
+ except KeyError:
await ctx.send("No quiz running.")
@quiz_game.command(name="leaderboard")
@@ -226,18 +492,20 @@ class TriviaQuiz(commands.Cog):
@staticmethod
async def send_score(channel: discord.TextChannel, player_data: dict) -> None:
- """A function which sends the score."""
+ """Send the current scores of players in the game channel."""
if len(player_data) == 0:
await channel.send("No one has made it onto the leaderboard yet.")
return
- embed = discord.Embed(colour=discord.Colour.blue())
- embed.title = "Score Board"
- embed.description = ""
+ embed = discord.Embed(
+ colour=Colours.blue,
+ title="Score Board",
+ description="",
+ )
- sorted_dict = sorted(player_data.items(), key=lambda a: a[1], reverse=True)
+ sorted_dict = sorted(player_data.items(), key=operator.itemgetter(1), reverse=True)
for item in sorted_dict:
- embed.description += f"{item[0]} : {item[1]}\n"
+ embed.description += f"{item[0]}: {item[1]}\n"
await channel.send(embed=embed)
@@ -250,7 +518,6 @@ class TriviaQuiz(commands.Cog):
# Check if more than 1 player has highest points.
if no_of_winners > 1:
- word = "You guys"
winners = []
points_copy = list(player_data.values()).copy()
@@ -261,41 +528,62 @@ class TriviaQuiz(commands.Cog):
winners_mention = " ".join(winner.mention for winner in winners)
else:
- word = "You"
author_index = list(player_data.values()).index(highest_points)
winner = list(player_data.keys())[author_index]
winners_mention = winner.mention
await channel.send(
f"Congratulations {winners_mention} :tada: "
- f"{word} have won this quiz game with a grand total of {highest_points} points!"
+ f"You have won this quiz game with a grand total of {highest_points} points!"
)
def category_embed(self) -> discord.Embed:
"""Build an embed showing all available trivia categories."""
- embed = discord.Embed(colour=discord.Colour.blue())
- embed.title = "The available question categories are:"
+ embed = discord.Embed(
+ colour=Colours.blue,
+ title="The available question categories are:",
+ description="",
+ )
+
embed.set_footer(text="If a category is not chosen, a random one will be selected.")
- embed.description = ""
for cat, description in self.categories.items():
- embed.description += f"**- {cat.capitalize()}**\n{description.capitalize()}\n"
+ embed.description += (
+ f"**- {cat.capitalize()}**\n"
+ f"{description.capitalize()}\n"
+ )
return embed
@staticmethod
- async def send_answer(channel: discord.TextChannel, question_dict: dict) -> None:
+ async def send_answer(
+ channel: discord.TextChannel,
+ answers: List[str],
+ answer_is_correct: bool,
+ question_dict: dict,
+ q_left: int,
+ ) -> None:
"""Send the correct answer of a question to the game channel."""
- answer = question_dict["answer"]
- info = question_dict["info"]
- embed = discord.Embed(color=discord.Colour.red())
- embed.title = f"The correct answer is **{answer}**\n"
- embed.description = ""
+ info = question_dict.get("info")
+
+ plurality = " is" if len(answers) == 1 else "s are"
- if info != "":
+ embed = discord.Embed(
+ color=Colours.bright_green,
+ title=(
+ ("You got it! " if answer_is_correct else "")
+ + f"The correct answer{plurality} **`{', '.join(answers)}`**\n"
+ ),
+ description="",
+ )
+
+ if info is not None:
embed.description += f"**Information**\n{info}\n\n"
- embed.description += "Let's move to the next question.\nRemaining questions: "
+ embed.description += (
+ ("Let's move to the next question." if q_left > 0 else "")
+ + f"\nRemaining questions: {q_left}"
+ )
await channel.send(embed=embed)
diff --git a/bot/resources/evergreen/trivia_quiz.json b/bot/resources/evergreen/trivia_quiz.json
index a4225eb1..fee1b6d7 100644
--- a/bot/resources/evergreen/trivia_quiz.json
+++ b/bot/resources/evergreen/trivia_quiz.json
@@ -44,6 +44,24 @@
],
"question": "What was the first game Yoshi appeared in?",
"answer": "Super Mario World"
+ },
+ {
+ "id": 6,
+ "hints": [
+ "They were used alternatively to playing cards.",
+ "They generally have handdrawn nature images on them."
+ ],
+ "question": "What did Nintendo make before video games and toys?",
+ "answer": "Hanafuda, Hanafuda cards"
+ },
+ {
+ "id": 7,
+ "hints": [
+ "Before being Nintendo's main competitor in home gaming, they were successful in arcades.",
+ "Their first console was called the Master System."
+ ],
+ "question": "Who was Nintendo's biggest competitor in 1990?",
+ "answer": "Sega"
}
],
"general": [
@@ -269,5 +287,322 @@
"answer": "1492",
"info": "The explorer Christopher Columbus made four trips across the Atlantic Ocean from Spain: in 1492, 1493, 1498 and 1502. He was determined to find a direct water route west from Europe to Asia, but he never did. Instead, he stumbled upon the Americas"
}
+ ],
+ "math": [
+ {
+ "id": 201,
+ "question": "What is the highest power of a biquadratic polynomial?",
+ "answer": "4, four"
+ },
+ {
+ "id": 202,
+ "question": "What is the formula for surface area of a sphere?",
+ "answer": "4pir^2, 4πr^2"
+ },
+ {
+ "id": 203,
+ "question": "Which theorem states that hypotenuse^2 = base^2 + height^2?",
+ "answer": "Pythagorean's, Pythagorean's theorem"
+ },
+ {
+ "id": 204,
+ "question": "Which trigonometric function is defined as hypotenuse/opposite?",
+ "answer": "cosecant, cosec, csc"
+ },
+ {
+ "id": 205,
+ "question": "Does the harmonic series converge or diverge?",
+ "answer": "diverge"
+ },
+ {
+ "id": 206,
+ "question": "How many quadrants are there in a cartesian plane?",
+ "answer": "4, four"
+ },
+ {
+ "id": 207,
+ "question": "What is the (0,0) coordinate in a cartesian plane termed as?",
+ "answer": "origin"
+ },
+ {
+ "id": 208,
+ "question": "What's the following formula that finds the area of a triangle called?",
+ "img_url": "https://wikimedia.org/api/rest_v1/media/math/render/png/d22b8566e8187542966e8d166e72e93746a1a6fc",
+ "answer": "Heron's formula, Heron"
+ },
+ {
+ "id": 209,
+ "dynamic_id": 201,
+ "question": "Solve the following system of linear equations (format your answer like this <x> & <y>):\n{}x + {}y = {},\n{}x + {}y = {}",
+ "answer": "{} & {}"
+ },
+ {
+ "id": 210,
+ "dynamic_id": 202,
+ "question": "What's {} + {} mod {} congruent to?",
+ "answer": "{}"
+ },
+ {
+ "id": 211,
+ "question": "What is the bottom number on a fraction called?",
+ "answer": "denominator"
+ },
+ {
+ "id": 212,
+ "dynamic_id": 203,
+ "question": "How many vertices are on a {}gonal prism?",
+ "answer": "{}"
+ },
+ {
+ "id": 213,
+ "question": "What is the term used to describe two triangles that have equal corresponding sides and angle measures?",
+ "answer": "congruent"
+ },
+ {
+ "id": 214,
+ "question": "⅓πr^2h is the volume of which 3 dimensional figure?",
+ "answer": "cone"
+ },
+ {
+ "id": 215,
+ "dynamic_id": 204,
+ "question": "Find the square root of -{}.",
+ "answer": "{}i"
+ },
+ {
+ "id": 216,
+ "question": "In set builder notation, {p/q | q ≠ 0, p & q ∈ Z} represents what?",
+ "answer": "Rational Numbers"
+ },
+ {
+ "id": 217,
+ "question": "What is the natural log of -1 (use i for imaginary number)?",
+ "answer": "pi*i, pii, πi"
+ },
+ {
+ "id": 218,
+ "question": "When is the *inaugural* World Maths Day (format your answer in MM/DD)?",
+ "answer": "03/13"
+ },
+ {
+ "id": 219,
+ "question": "As the Fibonacci sequence extends to infinity, what's the ratio of each number `n` and its preceding number `n-1` approaching?",
+ "answer": "Golden Ratio"
+ },
+ {
+ "id": 220,
+ "question": "0, 1, 1, 2, 3, 5, 8, 13, 21, 34 are numbers of which sequence?",
+ "answer": "Fibonacci"
+ },
+ {
+ "id": 221,
+ "question": "Prime numbers only have __ factors.",
+ "answer": "2, two"
+ },
+ {
+ "id": 222,
+ "question": "In probability, the ________ ______ of an experiment or random trial is the set of all possible outcomes of it.",
+ "answer": "sample space"
+ },
+ {
+ "id": 223,
+ "question": "In statistics, what does this formula represent?",
+ "img_url": "https://www.statisticshowto.com/wp-content/uploads/2013/11/sample-standard-deviation.jpg",
+ "answer": "sample standard deviation, standard deviation of a sample"
+ },
+ {
+ "id": 224,
+ "question": "\"Hexakosioihexekontahexaphobia\" is the fear of which number?",
+ "answer": "666"
+ },
+ {
+ "id": 225,
+ "question": "A matrix multiplied by its inverse matrix equals...",
+ "answer": "the identity matrix, identity matrix"
+ },
+ {
+ "id": 226,
+ "dynamic_id": 205,
+ "question": "BASE TWO QUESTION: Calculate {:b} {} {:b}",
+ "answer": "{:b}"
+ },
+ {
+ "id": 227,
+ "question": "What is the only number in the entire number system which can be spelled with the same number of letters as itself?",
+ "answer": "4, four"
+
+ },
+ {
+ "id": 228,
+ "question": "1/100th of a second is also termed as what?",
+ "answer": "a jiffy, jiffy, centisecond"
+ },
+ {
+ "id": 229,
+ "question": "What is this triangle called?",
+ "img_url": "https://cdn.askpython.com/wp-content/uploads/2020/07/Pascals-triangle.png",
+ "answer": "Pascal's triangle, Pascal"
+ },
+ {
+ "id": 230,
+ "question": "6a^2 is the surface area of which 3 dimensional figure?",
+ "answer": "cube"
+ }
+ ],
+ "science": [
+ {
+ "id": 301,
+ "question": "The three main components of a normal atom are: protons, neutrons, and...",
+ "answer": "electrons"
+ },
+ {
+ "id": 302,
+ "question": "As of 2021, how many elements are there in the Periodic Table?",
+ "answer": "118"
+ },
+ {
+ "id": 303,
+ "question": "What is the universal force discovered by Newton that causes objects with mass to attract each other called?",
+ "answer": "gravity"
+ },
+ {
+ "id": 304,
+ "question": "What do you call an organism composed of only one cell?",
+ "answer": "unicellular, single-celled"
+ },
+ {
+ "id": 305,
+ "question": "The Heisenberg's Uncertainty Principle states that the position and ________ of a quantum object can't be both exactly measured at the same time.",
+ "answer": "velocity, momentum"
+ },
+ {
+ "id": 306,
+ "question": "A ____________ reaction is the one wherein an atom or a set of atoms is/are replaced by another atom or a set of atoms",
+ "answer": "displacement, exchange"
+ },
+ {
+ "id": 307,
+ "question": "What is the process by which green plants and certain other organisms transform light energy into chemical energy?",
+ "answer": "photosynthesis"
+ },
+ {
+ "id": 308,
+ "dynamic_id": 301,
+ "question": "What is the {} planet of our Solar System?",
+ "answer": "{}"
+ },
+ {
+ "id": 309,
+ "dynamic_id": 302,
+ "question": "In the biological taxonomic hierarchy, what is placed directly above {}?",
+ "answer": "{}"
+ },
+ {
+ "id": 310,
+ "dynamic_id": 303,
+ "question": "How does one describe the unit {} in SI base units?\n**IMPORTANT:** enclose answer in backticks, use \\* for multiplication, ^ for exponentiation, and place your base units in this order: m - kg - s - A",
+ "img_url": "https://i.imgur.com/NRzU6tf.png",
+ "answer": "`{}`"
+ },
+ {
+ "id": 311,
+ "question": "How does one call the direct phase transition from gas to solid?",
+ "answer": "deposition"
+ },
+ {
+ "id": 312,
+ "question": "What is the intermolecular force caused by temporary and induced dipoles?",
+ "answer": "LDF, London dispersion, London dispersion force"
+ },
+ {
+ "id": 313,
+ "question": "What is the force that causes objects to float in fluids called?",
+ "answer": "buoyancy"
+ },
+ {
+ "id": 314,
+ "question": "About how many neurons are in the human brain? (A. 1 billion, B. 10 billion, C. 100 billion, D. 300 billion)",
+ "answer": "C, 100 billion, 100 bil"
+ },
+ {
+ "id": 315,
+ "question": "What is the name of our galaxy group in which the Milky Way resides?",
+ "answer": "Local Group"
+ },
+ {
+ "id": 316,
+ "question": "Which cell organelle is nicknamed \"the powerhouse of the cell\"?",
+ "answer": "mitochondria"
+ },
+ {
+ "id": 317,
+ "question": "Which vascular tissue transports water and minerals from the roots to the rest of a plant?",
+ "answer": "the xylem, xylem"
+ },
+ {
+ "id": 318,
+ "question": "Who discovered the theories of relativity?",
+ "answer": "Albert Einstein, Einstein"
+ },
+ {
+ "id": 319,
+ "question": "In particle physics, the hypothetical isolated elementary particle with only one magnetic pole is termed as...",
+ "answer": "magnetic monopole"
+ },
+ {
+ "id": 320,
+ "question": "How does one describe a chemical reaction wherein heat is released?",
+ "answer": "exothermic"
+ },
+ {
+ "id": 321,
+ "question": "What range of frequency are the average human ears capable of hearing (A. 10Hz-10kHz, B. 20Hz-20kHz, C. 20Hz-2000Hz, D. 10kHz-20kHz)?",
+ "answer": "B, 20Hz-20kHz"
+ },
+ {
+ "id": 322,
+ "question": "What is the process used to separate substances with different polarity in a mixture, using a stationary and mobile phase?",
+ "answer": "chromatography"
+ },
+ {
+ "id": 323,
+ "question": "Which law states that the current through a conductor between two points is directly proportional to the voltage across the two points?",
+ "answer": "Ohm's law"
+ },
+ {
+ "id": 324,
+ "question": "The type of rock that is formed by the accumulation or deposition of mineral or organic particles at the Earth's surface, followed by cementation, is called...",
+ "answer": "sedimentary, sedimentary rock"
+ },
+ {
+ "id": 325,
+ "question": "Is the Richter scale (common earthquake scale) linear or logarithmic?",
+ "answer": "logarithmic"
+ },
+ {
+ "id": 326,
+ "question": "What type of image is formed by a convex mirror?",
+ "answer": "virtual image, virtual"
+ },
+ {
+ "id": 327,
+ "question": "How does one call the branch of physics that deals with the study of mechanical waves in gases, liquids, and solids including topics such as vibration, sound, ultrasound and infrasound",
+ "answer": "acoustics"
+ },
+ {
+ "id": 328,
+ "question": "Which law states that the global entropy in a closed system can only increase?",
+ "answer": "second law, second law of thermodynamics"
+ },
+ {
+ "id": 329,
+ "question": "Which particle is emitted during the beta decay of a radioactive element?",
+ "answer": "an electron, the electron, electron"
+ },
+ {
+ "id": 330,
+ "question": "When DNA is unzipped, two strands are formed. What are they called (separate both answers by the word \"and\")?",
+ "answer": "leading and lagging, leading strand and lagging strand"
+ }
]
}
diff --git a/bot/utils/pagination.py b/bot/utils/pagination.py
index 742281d7..d9c0862a 100644
--- a/bot/utils/pagination.py
+++ b/bot/utils/pagination.py
@@ -27,7 +27,14 @@ class EmptyPaginatorEmbed(Exception):
class LinePaginator(Paginator):
"""A class that aids in paginating code blocks for Discord messages."""
- def __init__(self, prefix: str = "```", suffix: str = "```", max_size: int = 2000, max_lines: int = None):
+ def __init__(
+ self,
+ prefix: str = '```',
+ suffix: str = '```',
+ max_size: int = 2000,
+ max_lines: Optional[int] = None,
+ linesep: str = "\n"
+ ):
"""
Overrides the Paginator.__init__ from inside discord.ext.commands.
@@ -36,9 +43,13 @@ class LinePaginator(Paginator):
`max_size` and `max_lines` denote the maximum amount of codepoints and lines
allowed per page.
"""
- self.prefix = prefix
- self.suffix = suffix
- self.max_size = max_size - len(suffix)
+ super().__init__(
+ prefix,
+ suffix,
+ max_size - len(suffix),
+ linesep
+ )
+
self.max_lines = max_lines
self._current_page = [prefix]
self._linecount = 0