aboutsummaryrefslogtreecommitdiffstats
path: root/bot/exts/events/trivianight/_questions.py
diff options
context:
space:
mode:
Diffstat (limited to 'bot/exts/events/trivianight/_questions.py')
-rw-r--r--bot/exts/events/trivianight/_questions.py253
1 files changed, 64 insertions, 189 deletions
diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py
index 0ab657d2..1d7bd4a9 100644
--- a/bot/exts/events/trivianight/_questions.py
+++ b/bot/exts/events/trivianight/_questions.py
@@ -1,4 +1,5 @@
from random import choice, randrange
+from string import ascii_uppercase
from time import perf_counter
from typing import Optional, TypedDict, Union
@@ -9,28 +10,18 @@ from discord.ui import Button, View
from bot.constants import Colours, NEGATIVE_REPLIES
from . import UserScore
+from ._game import Question, AlreadyUpdated, QuestionClosed
from ._scoreboard import Scoreboard
-class CurrentQuestion(TypedDict):
- """Representing the different 'keys' of the question taken from the JSON."""
+class AnswerButton(Button):
+ """Button subclass that's used to guess on a particular answer."""
- number: str
- description: str
- answers: list[str]
- correct: str
- points: Optional[int]
- time: Optional[int]
-
-
-class QuestionButton(Button):
- """Button subclass for the options of the questions."""
-
- def __init__(self, label: str, users_picked: dict, view: View):
- self.users_picked = users_picked
- self._view = view
+ def __init__(self, label: str, question: Question):
super().__init__(label=label, style=discord.ButtonStyle.green)
+ self.question = question
+
async def callback(self, interaction: Interaction) -> None:
"""
When a user interacts with the button, this will be called.
@@ -39,114 +30,108 @@ class QuestionButton(Button):
- interaction: an instance of discord.Interaction representing the interaction between the user and the
button.
"""
- original_message = interaction.message
- original_embed = original_message.embeds[0]
-
- if interaction.user.id not in self.users_picked.keys():
- people_answered = original_embed.footer.text
- people_answered = f"{int(people_answered[0]) + 1} " \
- f"{'person has' if int(people_answered[0]) + 1 == 1 else 'people have'} answered"
- original_embed.set_footer(text=people_answered)
- await original_message.edit(embed=original_embed, view=self._view)
- self.users_picked[interaction.user.id] = [self.label, True, perf_counter() - self._time]
+ try:
+ guess = self.question.guess(interaction.user.id, self.label)
+ except AlreadyUpdated:
await interaction.response.send_message(
embed=Embed(
- title="Confirming that..",
- description=f"You chose answer {self.label}.",
- color=Colours.soft_green
+ title=choice(NEGATIVE_REPLIES),
+ description="You've already changed your answer more than once!",
+ color=Colours.soft_red
),
ephemeral=True
)
- elif self.users_picked[interaction.user.id][1] is True:
- self.users_picked[interaction.user.id] = [
- self.label, False, perf_counter() - self._time
- ]
+ return
+ except QuestionClosed:
+ await interaction.response.send_message(
+ embed=Embed(
+ title=choice(NEGATIVE_REPLIES),
+ description="The question is no longer accepting guesses!",
+ color=Colours.soft_red
+ ),
+ )
+ return
+
+ if guess[1]:
await interaction.response.send_message(
embed=Embed(
title="Confirming that..",
- description=f"You changed your answer to answer {self.label}.",
+ description=f"You chose answer {self.label}.",
color=Colours.soft_green
),
ephemeral=True
)
else:
+ # guess[1] is False and they cannot change their answer again. Which
+ # indicates that they changed it this time around.
await interaction.response.send_message(
embed=Embed(
- title=choice(NEGATIVE_REPLIES),
- description="You've already changed your answer more than once!",
- color=Colours.soft_red
+ title="Confirming that..",
+ description=f"You changed your answer to answer {self.label}.",
+ color=Colours.soft_green
),
ephemeral=True
)
class QuestionView(View):
- """View for the questions."""
+ """View for one trivia night question."""
- def __init__(self):
+ def __init__(self, question: Question) -> None:
super().__init__()
- self.current_question: CurrentQuestion
- self.users_picked = {}
+ self.question = question
- self.active_question = False
+ for letter, _ in self.question.answers:
+ self.add_item(AnswerButton(letter, self.question))
- def create_current_question(self) -> tuple[Embed, int]:
+ @staticmethod
+ def unicodeify(text: str) -> str:
"""
- Helper function to create the embed for the current question.
+ Takes `text` and adds zero-width spaces to prevent copy and pasting the question.
- Returns an embed containing the question along with each answer choice in the form of a view,
- along with the integer representing the time limit of the current question.
+ Parameters:
+ - text: A string that represents the question description to 'unicodeify'
"""
- self.current_labels = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[:len(self.current_question["answers"])]
+ return "".join(
+ f"{letter}\u200b" if letter not in ('\n', '\t', '`', 'p', 'y') else letter
+ for idx, letter in enumerate(text)
+ )
+
+ def create_embed(self) -> Embed:
+ """Helper function to create the embed for the current question."""
question_embed = Embed(
- title=f"Question {self.current_question['number']}",
- description=self.current_question["obfuscated_description"],
+ title=f"Question {self.question.number}",
+ description=self.unicodeify(self.question.description),
color=Colours.python_yellow
)
- if "_previously_visited" not in self.current_question.keys():
- self.buttons = [QuestionButton(label, self.users_picked, self) for label in self.current_labels]
- for button in self.buttons:
- self.add_item(button)
- for label, answer in zip(self.current_labels, self.current_question["answers"]):
+ for label, answer in self.question.answers:
question_embed.add_field(name=f"Answer {label}", value=answer, inline=False)
- question_embed.set_footer(text="0 people have answered")
- current_time = perf_counter()
- for button in self.buttons:
- button._time = current_time
-
- time_limit = self.current_question.get("time", 10)
-
- self.active_question = True
+ return question_embed
- return question_embed, time_limit
-
- def end_question(self) -> tuple[dict, Embed, int, int]:
+ def end_question(self) -> Embed:
"""
Ends the question and displays the statistics on who got the question correct, awards points, etc.
Returns:
- - a dictionary containing all the people who answered and whether or not they got it correct
- - an embed displaying the correct answers and the % of people that chose each answer.
- - an integer showing the time limit of the current question in seconds
- - an integer showing the amount of points the question will award* to those that got it correct
+ An embed displaying the correct answers and the % of people that chose each answer.
"""
- labels = self.current_labels
- label = labels[self.current_question["answers"].index(self.current_question["correct"])]
- return_dict = {name: (*info, info[0] == label) for name, info in self.users_picked.items()}
- all_players = list(self.users_picked.items())
+ guesses = self.question.stop()
+
+ labels = ascii_uppercase[:len(self.question.answers)]
+ correct = [label for (label, description) in self.question.answers if description == self.question.correct]
answer_embed = Embed(
- title=f"The correct answer for Question {self.current_question['number']} was..",
- description=self.current_question["correct"]
+ title=f"The correct answer for Question {self.question.number} was..",
+ description=self.question.correct
)
- if len(all_players) != 0:
+ if len(guesses) != 0:
answers_chosen = {
answer_choice: len(
- tuple(filter(lambda x: x[0] == answer_choice, self.users_picked.values()))
- ) / len(all_players)
+ tuple(filter(lambda x: x[0] == correct, guesses.values()))
+ ) / len(guesses)
for answer_choice in labels
}
@@ -160,118 +145,8 @@ class QuestionView(View):
# The `ord` function is used here to change the letter to its corresponding position
answer_embed.add_field(
name=f"{percent * 100:.1f}% of players chose",
- value=self.current_question['answers'][ord(answer) - 65],
+ value=self.question.answers[ord(answer) - 65][1],
inline=False
)
- self.users_picked = {}
-
- for button in self.buttons:
- button.users_picked = self.users_picked
-
- time_limit = self.current_question.get("time", 10)
- question_points = self.current_question.get("points", 10)
-
- self.active_question = False
-
- return return_dict, answer_embed, time_limit, question_points
-
-
-class Questions:
- """An interface to use from the TriviaNight cog for questions."""
-
- def __init__(self, scoreboard: Scoreboard):
- self.scoreboard = scoreboard
- self.questions = []
-
- def set_questions(self, questions: list) -> None:
- """
- Setting `self.questions` dynamically via a function to set it.
-
- Parameters:
- - questions: a list representing all the questions, which is essentially the JSON provided
- to load the questions
- """
- self.questions = questions
-
- def next_question(self, number: int = None) -> Union[Embed, None]:
- """
- Chooses a random unvisited question from the question bank.
-
- If the number parameter is specified, it'll head to that specific question.
-
- Parameters:
- - number: An optional integer representing the question number (only used for `.trivianight question` calls)
- """
- if all("visited" in question.keys() for question in self.questions) and number is None:
- return Embed(
- title=choice(NEGATIVE_REPLIES),
- description="All of the questions in the question bank have been used.",
- color=Colours.soft_red
- )
-
- if number is None:
- question_number = randrange(0, len(self.questions))
- while "visited" in self.questions[question_number].keys():
- question_number = randrange(0, len(self.questions))
- else:
- question_number = number - 1
-
- if "visited" in self.questions[question_number].keys():
- self.questions[question_number]["_previously_visited"] = True
- self.questions[question_number]["visited"] = True
-
- # The `self.view` refers to the QuestionView
- self.view.current_question = self.questions[question_number]
-
- def list_questions(self) -> Union[Embed, str]:
- """
- Lists all questions from the question bank.
-
- It will put the following into a message:
- - Question number
- - Question description
- - If the question was already 'visited' (displayed)
- """
- if not self.questions:
- return Embed(
- title=choice(NEGATIVE_REPLIES),
- description="No questions are currently loaded in!",
- color=Colours.soft_red
- )
- spaces = len(
- sorted(
- self.questions, key=lambda question: len(question['description'])
- )[-1]["description"]
- ) + 3
- formatted_string = ""
- for question in self.questions:
- question_description = question["description"]
- formatted_string += f"`Q{question['number']}: {question_description}" \
- f"{' ' * (spaces - len(question_description) + 2)}" \
- f"|` {':x:' if not question.get('visited') else ':white_check_mark:'}\n"
-
- return formatted_string.strip()
-
- def current_question(self) -> tuple[Embed, QuestionView]:
- """Returns an embed entailing the current question as an embed with a view."""
- return self.view.create_current_question(), self.view
-
- def end_question(self) -> Embed:
- """
- Terminates answering of the question and displays the correct answer.
-
- The function returns an embed containing the information about the question such as the following:
- - % of people that chose each option
- - the correct answer
- """
- scores, answer_embed, time_limit, total_points = self.view.end_question()
- for user, score in scores.items():
- time_taken = score[2]
- point_calculation = (1 - (time_taken / time_limit) / 2) * total_points
- if score[-1] is True:
- self.scoreboard[UserScore(user)] = {"points": point_calculation, "speed": time_taken}
- elif score[-1] is False and score[2] <= 2:
- # Get the negative of the point_calculation to deduct it
- self.scoreboard[UserScore(user)] = {"points": -point_calculation}
return answer_embed