diff options
| -rw-r--r-- | bot/exts/evergreen/trivia_quiz.py | 244 | 
1 files changed, 184 insertions, 60 deletions
| diff --git a/bot/exts/evergreen/trivia_quiz.py b/bot/exts/evergreen/trivia_quiz.py index fe692c2a..2158ac9c 100644 --- a/bot/exts/evergreen/trivia_quiz.py +++ b/bot/exts/evergreen/trivia_quiz.py @@ -9,11 +9,12 @@ from discord.ext import commands  from fuzzywuzzy import fuzz  from bot.constants import Roles +from bot.constants import NEGATIVE_REPLIES +from typing import Optional, List, Tuple  logger = logging.getLogger(__name__) -  WRONG_ANS_RESPONSE = [      "No one answered correctly!",      "Better luck next time" @@ -25,34 +26,58 @@ class TriviaQuiz(commands.Cog):      def __init__(self, bot: commands.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 = 7 +          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!",          } +    @commands.command() +    async def test(self, ctx: commands.Context) -> None: +        embed = discord.Embed( +            title="yeet", +            description="yeet", +        ) + +        print("yeet") +        embed.set_image(url="") + +        await ctx.send(embed=embed) +      @staticmethod      def load_questions() -> dict:          """Load the questions from the JSON file."""          p = Path("bot", "resources", "evergreen", "trivia_quiz.json") +          with p.open(encoding="utf8") as json_data:              questions = json.load(json_data)              return questions      @commands.group(name="quiz", aliases=["trivia"], invoke_without_command=True) -    async def quiz_game(self, ctx: commands.Context, category: str = None) -> None: +    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:              self.game_status[ctx.channel.id] = False @@ -61,11 +86,13 @@ class TriviaQuiz(commands.Cog):          # Stop game if running.          if self.game_status[ctx.channel.id] is True: -            return await ctx.send( -                f"Game is already running..." +            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)) @@ -76,6 +103,30 @@ class TriviaQuiz(commands.Cog):              await ctx.send(embed=embed)              return +        topic = self.questions[category] +        topic_length = len(topic) + +        if questions: +            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( +                        "Please do at least one question." +                    ) +                ) +                return + +            else: +                self.question_limit = questions - 1 +          # Start game if not running.          if self.game_status[ctx.channel.id] is False:              self.game_owners[ctx.channel.id] = ctx.author @@ -85,16 +136,17 @@ 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:                  await ctx.send("The round has ended.") -                await self.declare_winner(ctx.channel, self.game_player_scores[ctx.channel.id]) +                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] @@ -111,23 +163,39 @@ 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: +                    q = question_dict["question"] +                    answers = question_dict["answer"].split(", ") +                else: +                    q, answers = DYNAMIC_QUESTIONS_FORMAT_FUNCS[ +                        question_dict["dynamic_id"] +                    ]( +                        question_dict["question"], +                        question_dict["answer"], +                    ) + +                    answers = [answers] + +                embed = discord.Embed( +                    colour=discord.Colour.gold(), +                    title=f"Question #{len(done_question)}", +                    description=q, +                ) + +                if "img_url" in question_dict: +                    embed.set_image(url=question_dict["img_url"]) -                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.              # 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 +                return m.channel == ctx.channel and any( +                    fuzz.ratio(answer.lower(), m.content.lower()) > VARIATION_TOLERANCE +                    for answer in answers                  )              try: -                msg = await self.bot.wait_for('message', check=check, timeout=10) +                msg = await self.bot.wait_for("message", check=check, 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: @@ -136,9 +204,14 @@ class TriviaQuiz(commands.Cog):                  # 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**" +                            f"{hints[hint_no - 1]}" +                        )                      else:                          await ctx.send(f"{30 - hint_no * 10}s left!") @@ -151,19 +224,22 @@ 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, question_dict)                      await asyncio.sleep(1)                      hint_no = 0  # init hint_no = 0 so that 2 hints/time alerts can be sent for the new question. -                    await self.send_score(ctx.channel, self.game_player_scores[ctx.channel.id]) +                    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                  # 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: @@ -177,24 +253,49 @@ 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_score(ctx.channel, self.game_player_scores[ctx.channel.id]) +                await ctx.send( +                    f"{msg.author.mention} got the correct answer :tada: {points} points!" +                ) + +                await self.send_answer(ctx.channel, answers, question_dict) +                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=discord.Colour.red(), +            title="Quiz game starting!", +            description=( +                f"Each 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=discord.Colour.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:          """ @@ -204,18 +305,23 @@ class TriviaQuiz(commands.Cog):          """          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) +            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]) +                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:!") +                await ctx.send( +                    f"{ctx.author.mention}, you are not authorised to stop this game :ghost:!" +                )          else:              await ctx.send("No quiz running.") @@ -231,13 +337,15 @@ class TriviaQuiz(commands.Cog):              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=discord.Colour.blue(), +            title="Score Board", +            description="", +        )          sorted_dict = sorted(player_data.items(), key=lambda a: a[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 +358,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,36 +368,53 @@ 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.set_footer(text="If a category is not chosen, a random one will be selected.") -        embed.description = "" +        embed = discord.Embed( +            colour=discord.Colour.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{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], +            question_dict: dict, +    ) -> 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 = "" +        if "info" in question_dict: +            info = question_dict["info"] +        else: +            info = "" + +        embed = discord.Embed( +            color=discord.Colour.red(), +            title=f"The correct answer is/are **{', '.join(answers)}**\n", +            description="", +        )          if info != "":              embed.description += f"**Information**\n{info}\n\n" | 
