diff options
Diffstat (limited to 'bot/exts/events/trivianight/_questions.py')
-rw-r--r-- | bot/exts/events/trivianight/_questions.py | 253 |
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 |