diff options
author | 2021-09-05 00:31:20 -0400 | |
---|---|---|
committer | 2021-09-05 00:31:20 -0400 | |
commit | 02512e43f3d68ffd89654c5f2e9e3e9a27c0c018 (patch) | |
tree | 4b62a6dbb39601f02aa435c7eb8a10433585c3bb /bot/exts/evergreen/trivia_quiz.py | |
parent | Move snakes commands into fun folder (diff) |
Move game and fun commands to Fun folder, fix ddg
This moves all the fun commands and games into the fun folder.
This commit also makes changes to the duck_game.
It was setting a footer during an embed init, which is no longer
possible with the version of d.py we use. Additionally, an issue with
editing an embed that had a local image loaded.
The workaround for the time being is to update the message,
not the embed.
Diffstat (limited to 'bot/exts/evergreen/trivia_quiz.py')
-rw-r--r-- | bot/exts/evergreen/trivia_quiz.py | 593 |
1 files changed, 0 insertions, 593 deletions
diff --git a/bot/exts/evergreen/trivia_quiz.py b/bot/exts/evergreen/trivia_quiz.py deleted file mode 100644 index aa4020d6..00000000 --- a/bot/exts/evergreen/trivia_quiz.py +++ /dev/null @@ -1,593 +0,0 @@ -import asyncio -import json -import logging -import operator -import random -from dataclasses import dataclass -from pathlib import Path -from typing import Callable, Optional - -import discord -from discord.ext import commands -from rapidfuzz import fuzz - -from bot.bot import Bot -from bot.constants import Colours, NEGATIVE_REPLIES, Roles - -logger = logging.getLogger(__name__) - -DEFAULT_QUESTION_LIMIT = 6 -STANDARD_VARIATION_TOLERANCE = 88 -DYNAMICALLY_GEN_VARIATION_TOLERANCE = 97 - -WRONG_ANS_RESPONSE = [ - "No one answered correctly!", - "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): - self.bot = bot - - 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.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.", - "math": "General questions about mathematics ranging from grade 8 to grade 12.", - "science": "Put your understanding of science to the test!", - "cs": "A large variety of computer science questions.", - "python": "Trivia on our amazing language, Python!", - } - - @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(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. - - 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! - - cs: A large variety of computer science questions. - - python: Trivia on our amazing language, Python! - - (More to come!) - """ - if ctx.channel.id not in self.game_status: - self.game_status[ctx.channel.id] = False - - if ctx.channel.id not in self.game_player_scores: - self.game_player_scores[ctx.channel.id] = {} - - # Stop game if running. - if self.game_status[ctx.channel.id]: - await ctx.send( - "Game is already running... " - f"do `{self.bot.command_prefix}quiz stop`" - ) - return - - # Send embed showing available categories if inputted category is invalid. - if category is None: - category = random.choice(list(self.categories)) - - category = category.lower() - if category not in self.categories: - embed = self.category_embed() - 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 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) - - await ctx.send(embed=start_embed) # send an embed with the rules - await asyncio.sleep(5) - - done_question = [] - hint_no = 0 - 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: - await ctx.send("The round has ended.") - 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] = {} - - break - - # If no hint has been sent or any time alert. Basically if hint_no = 0 means it is a new question. - if hint_no == 0: - # Select a random question which has not been used yet. - while True: - question_dict = random.choice(topic) - if question_dict["id"] not in done_question: - done_question.append(question_dict["id"]) - break - - 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"], - ) - - question, answers = quiz_entry.question, quiz_entry.answer - answers = [answers] - - 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_func(var_tol), timeout=10) - except asyncio.TimeoutError: - # In case of TimeoutError and the game has been stopped, then do nothing. - if not self.game_status[ctx.channel.id]: - break - - if hint_no < 2: - hint_no += 1 - - if "hints" in question_dict: - hints = question_dict["hints"] - - await ctx.send(f"**Hint #{hint_no}\n**{hints[hint_no - 1]}") - else: - await ctx.send(f"{30 - hint_no * 10}s left!") - - # Once hint or time alerts has been sent 2 times, the hint_no value will be 3 - # If hint_no > 2, then it means that all hints/time alerts have been sent. - # Also means that the answer is not yet given and the bot sends the answer and the next question. - else: - if self.game_status[ctx.channel.id] is False: - break - - response = random.choice(WRONG_ANS_RESPONSE) - await ctx.send(response) - - await self.send_answer( - ctx.channel, - answers, - False, - question_dict, - self.question_limit - len(done_question) + 1, - ) - await asyncio.sleep(1) - - 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) - else: - if self.game_status[ctx.channel.id] is False: - break - - 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: - self.game_player_scores[ctx.channel.id][msg.author] = points - - # Also updating the overall scoreboard. - if msg.author in self.player_scores: - self.player_scores[msg.author] += points - else: - self.player_scores[msg.author] = points - - hint_no = 0 - - await ctx.send(f"{msg.author.mention} got the correct answer :tada: {points} points!") - - 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) - - def make_start_embed(self, category: str) -> discord.Embed: - """Generate a starting/introduction embed for the quiz.""" - start_embed = discord.Embed( - colour=Colours.blue, - title="A quiz game is starting!", - description=( - f"This game consists of {self.question_limit + 1} questions.\n\n" - "**Rules: **\n" - "1. Only enclose your answer in backticks when the question tells you to.\n" - "2. If the question specifies an answer format, follow it or else it won't be accepted.\n" - "3. You have 30s per question. Points for each question reduces by 25 after 10s or after a hint.\n" - "4. No cheating and have fun!\n\n" - f"**Category**: {category}" - ), - ) - - 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: - """ - Stop a quiz game if its running in the channel. - - Note: Only mods or the owner of the quiz can stop it. - """ - 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]) - - 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") - async def leaderboard(self, ctx: commands.Context) -> None: - """View everyone's score for this bot session.""" - await self.send_score(ctx.channel, self.player_scores) - - @staticmethod - async def send_score(channel: discord.TextChannel, player_data: dict) -> None: - """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=Colours.blue, - title="Score Board", - description="", - ) - - 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" - - await channel.send(embed=embed) - - @staticmethod - async def declare_winner(channel: discord.TextChannel, player_data: dict) -> None: - """Announce the winner of the quiz in the game channel.""" - if player_data: - highest_points = max(list(player_data.values())) - no_of_winners = list(player_data.values()).count(highest_points) - - # Check if more than 1 player has highest points. - if no_of_winners > 1: - winners = [] - points_copy = list(player_data.values()).copy() - - for _ in range(no_of_winners): - index = points_copy.index(highest_points) - winners.append(list(player_data.keys())[index]) - points_copy[index] = 0 - - winners_mention = " ".join(winner.mention for winner in winners) - else: - 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"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=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.") - - for cat, description in self.categories.items(): - embed.description += ( - f"**- {cat.capitalize()}**\n" - f"{description.capitalize()}\n" - ) - - return embed - - @staticmethod - 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.""" - info = question_dict.get("info") - - plurality = " is" if len(answers) == 1 else "s are" - - 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." if q_left > 0 else "") - + f"\nRemaining questions: {q_left}" - ) - await channel.send(embed=embed) - - -def setup(bot: Bot) -> None: - """Load the TriviaQuiz cog.""" - bot.add_cog(TriviaQuiz(bot)) |