From a7d00f8de9a5cfa2b9c76f1a2b39ac861787e24e Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 19 Oct 2021 21:43:53 -0400 Subject: add trivianight structure in the main cog --- bot/exts/events/trivianight/_questions.py | 116 ++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 bot/exts/events/trivianight/_questions.py (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py new file mode 100644 index 00000000..ef56ee81 --- /dev/null +++ b/bot/exts/events/trivianight/_questions.py @@ -0,0 +1,116 @@ +from random import choice +from time import perf_counter + +from discord import ButtonStyle, Embed, Interaction +from discord.ui import Button, View + +from bot.constants import Colours, NEGATIVE_REPLIES +from ._scoreboard import Scoreboard + + +class QuestionButton(Button): + """Button subclass for the options of the questions.""" + + def __init__(self, label: str): + self._time = perf_counter() + self.users_picked = {} + super().__init__(label=label, style=ButtonStyle.green) + + def answer(self, label: str) -> dict: + """Returns the dictionary of the users who picked the answer only if it was correct.""" + return self.users_picked if label == self.label else {} + + async def callback(self, interaction: Interaction) -> None: + """When a user interacts with the button, this will be called.""" + if interaction.user.id not in self.users_picked.keys(): + self.users_picked[interaction.user.id] = [self.label, 1, perf_counter() - self._time] + elif self.users_picked[interaction.user.id][1] < 3: + self.users_picked[interaction.user.id] = [ + self.label, self.users_picked[interaction.user.id][0] + 1, perf_counter() - self._time + ] + else: + 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 + ), + ephemeral=True + ) + + +class QuestionView(View): + """View for the questions.""" + + def __init__(self): + self.current_question = {} + + def create_current_question(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["description"], + color=Colours.python_yellow + ) + for label, answer in zip(("A", "B", "C", "D"), self.current_question["answers"]): + question_embed.add_field(name=label, value=answer, inline=False) + + self.buttons = [QuestionButton(label) for label in ("A", "B", "C", "D")] + for button in self.buttons: + self.add_item(button) + return question_embed + + def end_question(self) -> tuple[dict, Embed]: + """Returns the dictionaries from the corresponding buttons for those who got it correct.""" + labels = ("A", "B", "C", "D") + label = labels[self.current_question["correct"].index(self.current_question["answers"])] + return_dict = {} + for button in self.buttons: + return_dict.update(button.answer(label)) + self.remove_item(button) + + answer_embed = Embed( + title=f"The correct answer for Question {self.current_question['number']} was", + color=Colours.grass_green + ) + answer_embed.add_field( + name=label, + value=self.current_question["correct"].index(self.current_question["answers"]), + inline=False + ) + + return return_dict, answer_embed + + +class Questions: + """An interface to use from the TriviaNight cog for questions.""" + + def __init__(self, scoreboard: Scoreboard): + self.scoreboard = scoreboard + self.view = QuestionView() + self.questions = [] + self._ptr = -1 + + def set_questions(self, questions: list) -> None: + """Setting `self.questions` dynamically via a function to set it.""" + self.questions = questions + + def next_question(self) -> None: + """Advances to the next question.""" + self._ptr += 1 + if self._ptr < len(self.questions): + self.questions[self._ptr]["visited"] = True + self.view.current_question = self.questions[self._ptr] + + 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) -> None: + """Terminates answering of the question and displays the correct answer.""" + scores, answer_embed = self.view.end_question() + for user, score in scores.items(): + self.scoreboard[f"points: {user}"] = score[1] + self.scoreboard[f"speed: {user}"] = score[2] + + return answer_embed -- cgit v1.2.3 From 00b2f33e3366f885ad7d24bade546f604e065710 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Wed, 20 Oct 2021 20:44:42 -0400 Subject: added functionality trivianight next, trivianight stop, trivianight end added --- bot/exts/events/trivianight/_questions.py | 74 +++++++++++++++++------------- bot/exts/events/trivianight/_scoreboard.py | 30 +++++++----- bot/exts/events/trivianight/trivianight.py | 32 +++++++++++-- 3 files changed, 89 insertions(+), 47 deletions(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index ef56ee81..f558c50e 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -1,7 +1,9 @@ -from random import choice +from random import choice, randrange from time import perf_counter +from typing import Union -from discord import ButtonStyle, Embed, Interaction +import discord +from discord import Embed, Interaction from discord.ui import Button, View from bot.constants import Colours, NEGATIVE_REPLIES @@ -11,22 +13,21 @@ from ._scoreboard import Scoreboard class QuestionButton(Button): """Button subclass for the options of the questions.""" - def __init__(self, label: str): - self._time = perf_counter() - self.users_picked = {} - super().__init__(label=label, style=ButtonStyle.green) + def __init__(self, label: str, users_picked: dict): + self.users_picked = users_picked + super().__init__(label=label, style=discord.ButtonStyle.green) - def answer(self, label: str) -> dict: - """Returns the dictionary of the users who picked the answer only if it was correct.""" - return self.users_picked if label == self.label else {} + def set_time(self) -> None: + """Sets an instance attribute to a perf counter simulating the question beginning.""" + self._time = perf_counter() async def callback(self, interaction: Interaction) -> None: """When a user interacts with the button, this will be called.""" if interaction.user.id not in self.users_picked.keys(): self.users_picked[interaction.user.id] = [self.label, 1, perf_counter() - self._time] - elif self.users_picked[interaction.user.id][1] < 3: + elif self.users_picked[interaction.user.id][1] < 2: self.users_picked[interaction.user.id] = [ - self.label, self.users_picked[interaction.user.id][0] + 1, perf_counter() - self._time + self.label, self.users_picked[interaction.user.id][1] + 1, perf_counter() - self._time ] else: await interaction.response.send_message( @@ -43,7 +44,12 @@ class QuestionView(View): """View for the questions.""" def __init__(self): + super().__init__() self.current_question = {} + self.users_picked = {} + self.buttons = [QuestionButton(label, self.users_picked) for label in ("A", "B", "C", "D")] + for button in self.buttons: + self.add_item(button) def create_current_question(self) -> Embed: """Helper function to create the embed for the current question.""" @@ -53,30 +59,27 @@ class QuestionView(View): color=Colours.python_yellow ) for label, answer in zip(("A", "B", "C", "D"), self.current_question["answers"]): - question_embed.add_field(name=label, value=answer, inline=False) + question_embed.add_field(name=f"Choice {label}", value=answer, inline=False) - self.buttons = [QuestionButton(label) for label in ("A", "B", "C", "D")] for button in self.buttons: - self.add_item(button) + button.set_time() + return question_embed def end_question(self) -> tuple[dict, Embed]: """Returns the dictionaries from the corresponding buttons for those who got it correct.""" labels = ("A", "B", "C", "D") - label = labels[self.current_question["correct"].index(self.current_question["answers"])] - return_dict = {} + label = labels[self.current_question["answers"].index(self.current_question["correct"])] + return_dict = {name: info for name, info in self.users_picked.items() if info[0] == label} + self.users_picked = {} + for button in self.buttons: - return_dict.update(button.answer(label)) - self.remove_item(button) + button.users_picked = self.users_picked answer_embed = Embed( title=f"The correct answer for Question {self.current_question['number']} was", - color=Colours.grass_green - ) - answer_embed.add_field( - name=label, - value=self.current_question["correct"].index(self.current_question["answers"]), - inline=False + description=self.current_question["correct"], + color=Colours.soft_green ) return return_dict, answer_embed @@ -87,7 +90,6 @@ class Questions: def __init__(self, scoreboard: Scoreboard): self.scoreboard = scoreboard - self.view = QuestionView() self.questions = [] self._ptr = -1 @@ -95,18 +97,26 @@ class Questions: """Setting `self.questions` dynamically via a function to set it.""" self.questions = questions - def next_question(self) -> None: - """Advances to the next question.""" - self._ptr += 1 - if self._ptr < len(self.questions): - self.questions[self._ptr]["visited"] = True - self.view.current_question = self.questions[self._ptr] + def next_question(self) -> Union[Embed, None]: + """Uses another, new question.""" + if all("visited" in question.keys() for question in self.questions.values()): + return Embed( + title=choice(NEGATIVE_REPLIES), + description="All of the questions in the question bank have been used.", + color=Colours.soft_red + ) + + while "visited" in self.questions[self._ptr].keys(): + self._ptr = randrange(0, len(self.questions)) + + self.questions[self._ptr]["visited"] = True + self.view.current_question = self.questions[self._ptr] 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) -> None: + def end_question(self) -> Embed: """Terminates answering of the question and displays the correct answer.""" scores, answer_embed = self.view.end_question() for user, score in scores.items(): diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 96ff5ced..cab0288f 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -12,32 +12,41 @@ class ScoreboardView(View): """View for the scoreboard.""" def __init__(self, bot: Bot): + super().__init__() self.bot = bot self.points = {} self.speed = {} - def create_main_leaderboard(self) -> Embed: + async def create_main_leaderboard(self) -> Embed: """Helper function that iterates through `self.points` to generate the main leaderboard embed.""" main_embed = Embed( title="Winners of the Trivia Night", description="See the leaderboard for who got the most points during the Trivia Night!", color=Colours.python_blue, ) - for user, points in list(self.points.items())[:10]: - main_embed.add_field(name=self.bot.get_user(user), value=f"`{points}` pts", inline=False) + + # Limit self.points.items() to 10 items at maximum (top 10 users) in the future + for user, points in list(self.points.items()): + user = await self.bot.fetch_user(int(user)) + main_embed.add_field( + name=f"{user.name}#{user.discriminator}", + value=f"`{points}` pts", + inline=False + ) return main_embed - def _create_speed_embed(self) -> Embed: + async def _create_speed_embed(self) -> Embed: """Helper function that iterates through `self.speed` to generate a leaderboard embed.""" speed_embed = Embed( title="Average Time Taken to Answer a Question", description="See the leaderboard for how fast each user took to answer a question correctly!", color=Colours.python_blue, ) - for user, time_taken in list(self.speed.items())[:10]: + for user, time_taken in list(self.speed.items()): + user = await self.bot.fetch_user(int(user)) speed_embed.add_field( - name=self.bot.get_user(user), + name=f"{user.name}#{user.discriminator}", value=f"`{(time_taken[1] / time_taken[0]):.3f}s` (on average)", inline=False ) @@ -47,15 +56,12 @@ class ScoreboardView(View): @discord.ui.button(label="Scoreboard for Speed", style=ButtonStyle.green) async def speed_leaderboard(self, button: Button, interaction: Interaction) -> None: """Send an ephemeral message with the speed leaderboard embed.""" - await interaction.response.send_message(embed=self._create_speed_embed(), ephemeral=True) + await interaction.response.send_message(embed=await self._create_speed_embed(), ephemeral=True) class Scoreboard: """Class for the scoreboard for the trivianight event.""" - def __init__(self, bot: Bot): - self.view = ScoreboardView(bot) - def __setitem__(self, key: str, value: int): if key.startswith("points: "): key = key.removeprefix("points: ") @@ -76,6 +82,6 @@ class Scoreboard: elif item.startswith("speed: "): return self.view.speed[item.removeprefix("speed: ")] - def display(self) -> Union[Embed, View]: + async def display(self) -> Union[Embed, View]: """Returns the embed of the main leaderboard along with the ScoreboardView.""" - return self.view.create_main_leaderboard(), self.view + return await self.view.create_main_leaderboard(), self.view diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 66b2ae43..609f6651 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -1,3 +1,4 @@ +import logging from json import loads from random import choice @@ -6,8 +7,8 @@ from discord.ext import commands from bot.bot import Bot from bot.constants import Colours, POSITIVE_REPLIES -from ._questions import Questions -from ._scoreboard import Scoreboard +from ._questions import QuestionView, Questions +from ._scoreboard import Scoreboard, ScoreboardView class TriviaNight(commands.Cog): @@ -15,7 +16,7 @@ class TriviaNight(commands.Cog): def __init__(self, bot: Bot): self.bot = bot - self.scoreboard = Scoreboard(self.bot) + self.scoreboard = Scoreboard() self.questions = Questions(self.scoreboard) @commands.group() @@ -28,6 +29,9 @@ class TriviaNight(commands.Cog): """Load the JSON file provided into the questions.""" json_text = (await ctx.message.attachments[0].read()).decode("utf8") serialized_json = loads(json_text) + self.questions.view = QuestionView() + logging.getLogger(__name__).debug(self.questions.view) + self.scoreboard.view = ScoreboardView(self.bot) self.questions.set_questions(serialized_json) success_embed = Embed( title=choice(POSITIVE_REPLIES), @@ -36,6 +40,28 @@ class TriviaNight(commands.Cog): ) await ctx.send(embed=success_embed) + @trivianight.command() + async def next(self, ctx: commands.Context) -> None: + """Gets a random question from the unanswered question list and lets user choose the answer.""" + next_question = self.questions.next_question() + if isinstance(next_question, Embed): + await ctx.send(embed=next_question) + return + + question_embed, question_view = self.questions.current_question() + await ctx.send(embed=question_embed, view=question_view) + + @trivianight.command() + async def stop(self, ctx: commands.Context) -> None: + """End the ongoing question to show the correct question.""" + await ctx.send(embed=self.questions.end_question()) + + @trivianight.command() + async def end(self, ctx: commands.Context) -> None: + """Ends the trivia night event and displays the scoreboard.""" + scoreboard_embed, scoreboard_view = await self.scoreboard.display() + await ctx.send(embed=scoreboard_embed, view=scoreboard_view) + def setup(bot: Bot) -> None: """Load the TriviaNight cog.""" -- cgit v1.2.3 From c00acc579015a5bdb407ab96d05a1368e3894c53 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sat, 23 Oct 2021 15:24:02 -0400 Subject: added question and list command --- bot/exts/events/trivianight/_questions.py | 72 +++++++++++++++++++++++++----- bot/exts/events/trivianight/_scoreboard.py | 17 +++---- bot/exts/events/trivianight/trivianight.py | 31 +++++++++++++ 3 files changed, 101 insertions(+), 19 deletions(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index f558c50e..df3f237a 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -1,6 +1,6 @@ from random import choice, randrange from time import perf_counter -from typing import Union +from typing import TypedDict, Union import discord from discord import Embed, Interaction @@ -10,6 +10,15 @@ from bot.constants import Colours, NEGATIVE_REPLIES from ._scoreboard import Scoreboard +class CurrentQuestion(TypedDict): + """Representing the different 'keys' of the question taken from the JSON.""" + + number: str + description: str + answers: list + correct: str + + class QuestionButton(Button): """Button subclass for the options of the questions.""" @@ -25,10 +34,26 @@ class QuestionButton(Button): """When a user interacts with the button, this will be called.""" if interaction.user.id not in self.users_picked.keys(): self.users_picked[interaction.user.id] = [self.label, 1, perf_counter() - self._time] + await interaction.response.send_message( + embed=Embed( + title="Success!", + description=f"You chose answer choice {self.label}.", + color=Colours.soft_green + ), + ephemeral=True + ) elif self.users_picked[interaction.user.id][1] < 2: self.users_picked[interaction.user.id] = [ self.label, self.users_picked[interaction.user.id][1] + 1, perf_counter() - self._time ] + await interaction.response.send_message( + embed=Embed( + title="Success!", + description=f"You changed your answer to answer choice {self.label}.", + color=Colours.soft_green + ), + ephemeral=True + ) else: await interaction.response.send_message( embed=Embed( @@ -45,7 +70,7 @@ class QuestionView(View): def __init__(self): super().__init__() - self.current_question = {} + self.current_question: CurrentQuestion self.users_picked = {} self.buttons = [QuestionButton(label, self.users_picked) for label in ("A", "B", "C", "D")] for button in self.buttons: @@ -91,26 +116,51 @@ class Questions: def __init__(self, scoreboard: Scoreboard): self.scoreboard = scoreboard self.questions = [] - self._ptr = -1 def set_questions(self, questions: list) -> None: """Setting `self.questions` dynamically via a function to set it.""" self.questions = questions - def next_question(self) -> Union[Embed, None]: - """Uses another, new question.""" - if all("visited" in question.keys() for question in self.questions.values()): + 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. + """ + if all("visited" in question.keys() for question in self.questions): return Embed( title=choice(NEGATIVE_REPLIES), description="All of the questions in the question bank have been used.", color=Colours.soft_red ) - while "visited" in self.questions[self._ptr].keys(): - self._ptr = randrange(0, len(self.questions)) + 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 + + self.questions[question_number]["visited"] = True + self.view.current_question = self.questions[question_number] + + def list_questions(self) -> 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) + """ + spaces = len(sorted(self.questions, key=lambda question: len(question['description']))[-1]["description"]) + 3 + formatted_string = "" + for question in self.questions: + formatted_string += f"`Q{question['number']}: {question['description']!r}" \ + f"{' ' * (spaces - len(question['description']) + 2)}" \ + f"|` {':x:' if not question.get('visited') else ':checkmark:'}\n" - self.questions[self._ptr]["visited"] = True - self.view.current_question = self.questions[self._ptr] + return formatted_string.strip() def current_question(self) -> tuple[Embed, QuestionView]: """Returns an embed entailing the current question as an embed with a view.""" @@ -120,7 +170,7 @@ class Questions: """Terminates answering of the question and displays the correct answer.""" scores, answer_embed = self.view.end_question() for user, score in scores.items(): - self.scoreboard[f"points: {user}"] = score[1] + self.scoreboard[f"points: {user}"] = 1 self.scoreboard[f"speed: {user}"] = score[2] return answer_embed diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index cab0288f..7eb7a6a8 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -25,14 +25,14 @@ class ScoreboardView(View): color=Colours.python_blue, ) - # Limit self.points.items() to 10 items at maximum (top 10 users) in the future - for user, points in list(self.points.items()): + current_placement = 1 + for user, points in self.points.items(): user = await self.bot.fetch_user(int(user)) main_embed.add_field( - name=f"{user.name}#{user.discriminator}", - value=f"`{points}` pts", - inline=False + name=f"{current_placement}. {user.name}#{user.discriminator}", + value=f"`{points}` pts" ) + current_placement += 1 return main_embed @@ -43,13 +43,14 @@ class ScoreboardView(View): description="See the leaderboard for how fast each user took to answer a question correctly!", color=Colours.python_blue, ) - for user, time_taken in list(self.speed.items()): + current_placement = 1 + for user, time_taken in self.speed.items(): user = await self.bot.fetch_user(int(user)) speed_embed.add_field( - name=f"{user.name}#{user.discriminator}", + name=f"{current_placement}. {user.name}#{user.discriminator}", value=f"`{(time_taken[1] / time_taken[0]):.3f}s` (on average)", - inline=False ) + current_placement += 1 return speed_embed diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 609f6651..2ec869ab 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -40,6 +40,20 @@ class TriviaNight(commands.Cog): ) await ctx.send(embed=success_embed) + @trivianight.command() + async def reset(self, ctx: commands.Context) -> None: + """Resets previous questions and scoreboards.""" + self.scoreboard.view = ScoreboardView(self.bot) + for question in self.questions.questions: + del question["visited"] + + success_embed = Embed( + title=choice(POSITIVE_REPLIES), + description="The scoreboards were reset and questions marked unvisited!", + color=Colours.soft_green + ) + await ctx.send(embed=success_embed) + @trivianight.command() async def next(self, ctx: commands.Context) -> None: """Gets a random question from the unanswered question list and lets user choose the answer.""" @@ -51,6 +65,23 @@ class TriviaNight(commands.Cog): question_embed, question_view = self.questions.current_question() await ctx.send(embed=question_embed, view=question_view) + @trivianight.command() + async def question(self, ctx: commands.Context, question_number: int) -> None: + """Gets a question from the question bank depending on the question number provided.""" + question = self.questions.next_question(question_number) + if isinstance(question, Embed): + await ctx.send(embed=question) + return + + question_embed, question_view = self.questions.current_question() + await ctx.send(embed=question_embed, view=question_view) + + @trivianight.command() + async def list(self, ctx: commands.Context) -> None: + """Displays all the questions from the question bank.""" + formatted_string = self.questions.list_questions() + await ctx.send(formatted_string) + @trivianight.command() async def stop(self, ctx: commands.Context) -> None: """End the ongoing question to show the correct question.""" -- cgit v1.2.3 From 0a4c259d4726a3eb044cb9f57b871f4d46bed328 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 26 Oct 2021 07:35:05 -0400 Subject: Better type hinting Co-authored-by: Bluenix --- bot/exts/events/trivianight/_questions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index df3f237a..73d147f0 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -15,7 +15,7 @@ class CurrentQuestion(TypedDict): number: str description: str - answers: list + answers: list[str] correct: str -- cgit v1.2.3 From 848cf63fa885dc51e08d8975b0e834b272cec52d Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 26 Oct 2021 07:36:46 -0400 Subject: Brevity with "answer choice" Co-authored-by: Bluenix --- bot/exts/events/trivianight/_questions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 73d147f0..0c81d6d3 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -37,7 +37,7 @@ class QuestionButton(Button): await interaction.response.send_message( embed=Embed( title="Success!", - description=f"You chose answer choice {self.label}.", + description=f"You chose answer {self.label}.", color=Colours.soft_green ), ephemeral=True -- cgit v1.2.3 From 2b614d8b1ae89fc7e7e85e318b238b464aeae8b4 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 26 Oct 2021 07:40:07 -0400 Subject: Brevity with iterables Co-authored-by: Bluenix --- bot/exts/events/trivianight/_questions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 0c81d6d3..49fc894e 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -83,7 +83,7 @@ class QuestionView(View): description=self.current_question["description"], color=Colours.python_yellow ) - for label, answer in zip(("A", "B", "C", "D"), self.current_question["answers"]): + for label, answer in zip("ABCD", self.current_question["answers"]): question_embed.add_field(name=f"Choice {label}", value=answer, inline=False) for button in self.buttons: -- cgit v1.2.3 From 314cde47e37688fd60173a6f7586f0ab2ba2002b Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 26 Oct 2021 07:41:09 -0400 Subject: Improving word choice (answer [x] rather than answer choice [x]) Co-authored-by: Bluenix --- bot/exts/events/trivianight/_questions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 49fc894e..58b6abdf 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -84,7 +84,7 @@ class QuestionView(View): color=Colours.python_yellow ) for label, answer in zip("ABCD", self.current_question["answers"]): - question_embed.add_field(name=f"Choice {label}", value=answer, inline=False) + question_embed.add_field(name=f"Answer {label}", value=answer, inline=False) for button in self.buttons: button.set_time() -- cgit v1.2.3 From d0c46d4fdf603c08fe50b836e4f8cef0ba9b9430 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 26 Oct 2021 07:42:10 -0400 Subject: Use partial ellipsis in revealing answer Co-authored-by: Bluenix --- bot/exts/events/trivianight/_questions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 58b6abdf..f94b340f 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -102,7 +102,7 @@ class QuestionView(View): button.users_picked = self.users_picked answer_embed = Embed( - title=f"The correct answer for Question {self.current_question['number']} was", + title=f"The correct answer for Question {self.current_question['number']} was..", description=self.current_question["correct"], color=Colours.soft_green ) -- cgit v1.2.3 From 805cb8025433c87454027aad4e70bbe72b86dbdb Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Wed, 27 Oct 2021 19:08:34 -0400 Subject: Bluenix's reviews' changes --- bot/exts/events/trivianight/_questions.py | 32 +++++++++------ bot/exts/events/trivianight/_scoreboard.py | 66 ++++++++++++++---------------- bot/exts/events/trivianight/trivianight.py | 3 +- 3 files changed, 51 insertions(+), 50 deletions(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index f94b340f..741e8422 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -7,9 +7,19 @@ from discord import Embed, Interaction from discord.ui import Button, View from bot.constants import Colours, NEGATIVE_REPLIES + from ._scoreboard import Scoreboard +class UserScore: + """Marker class for passing into the scoreboard to add points/record speed.""" + + __slots__ = ("user_id",) + + def __init__(self, user_id: int): + self.user_id = user_id + + class CurrentQuestion(TypedDict): """Representing the different 'keys' of the question taken from the JSON.""" @@ -26,30 +36,26 @@ class QuestionButton(Button): self.users_picked = users_picked super().__init__(label=label, style=discord.ButtonStyle.green) - def set_time(self) -> None: - """Sets an instance attribute to a perf counter simulating the question beginning.""" - self._time = perf_counter() - async def callback(self, interaction: Interaction) -> None: """When a user interacts with the button, this will be called.""" if interaction.user.id not in self.users_picked.keys(): - self.users_picked[interaction.user.id] = [self.label, 1, perf_counter() - self._time] + self.users_picked[interaction.user.id] = [self.label, True, perf_counter() - self._time] await interaction.response.send_message( embed=Embed( - title="Success!", + title="Confirming that..", description=f"You chose answer {self.label}.", color=Colours.soft_green ), ephemeral=True ) - elif self.users_picked[interaction.user.id][1] < 2: + elif self.users_picked[interaction.user.id][1] is True: self.users_picked[interaction.user.id] = [ - self.label, self.users_picked[interaction.user.id][1] + 1, perf_counter() - self._time + self.label, False, perf_counter() - self._time ] await interaction.response.send_message( embed=Embed( - title="Success!", - description=f"You changed your answer to answer choice {self.label}.", + title="Confirming that..", + description=f"You changed your answer to answer {self.label}.", color=Colours.soft_green ), ephemeral=True @@ -86,8 +92,9 @@ class QuestionView(View): for label, answer in zip("ABCD", self.current_question["answers"]): question_embed.add_field(name=f"Answer {label}", value=answer, inline=False) + current_time = perf_counter() for button in self.buttons: - button.set_time() + button._time = current_time return question_embed @@ -170,7 +177,6 @@ class Questions: """Terminates answering of the question and displays the correct answer.""" scores, answer_embed = self.view.end_question() for user, score in scores.items(): - self.scoreboard[f"points: {user}"] = 1 - self.scoreboard[f"speed: {user}"] = score[2] + self.scoreboard[UserScore(user)] = {"points": 1, "speed": score[2]} return answer_embed diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index c39fc666..dbec966d 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -20,36 +20,38 @@ class ScoreboardView(View): async def create_main_leaderboard(self) -> Embed: """Helper function that iterates through `self.points` to generate the main leaderboard embed.""" - main_embed = Embed( - title="Winners of the Trivia Night", - description="See the leaderboard for who got the most points during the Trivia Night!", - color=Colours.python_blue, - ) - formatted_string = "" - participant_points = list(self.points.items())[:30] if len(self.points.items()) > 30 else self.points.items() - for current_placement, (user, points) in participant_points: + for current_placement, (user, points) in enumerate(self.points.items()): + if current_placement + 1 > 30: + break + user = await self.bot.fetch_user(int(user)) formatted_string += f"`{current_placement + 1}`. {user.mention} " formatted_string += f"({points} pts)\n" if (current_placement + 1) % 10 == 0: formatted_string += "⎯⎯⎯⎯⎯⎯⎯⎯\n" - current_placement += 1 + + main_embed = Embed( + title="Winners of the Trivia Night", + description=formatted_string, + color=Colours.python_blue, + ) return main_embed async def _create_speed_embed(self) -> Embed: """Helper function that iterates through `self.speed` to generate a leaderboard embed.""" formatted_string = "" - participant_speed = list(self.speed.items())[:30] if len(self.speed.items()) > 30 else self.speed.items() - for current_placement, (user, time_taken) in enumerate(participant_speed): + for current_placement, (user, time_taken) in enumerate(self.speed.items()): + if current_placement + 1 > 30: + break + user = await self.bot.fetch_user(int(user)) formatted_string += f"`{current_placement + 1}`. {user.mention} " - formatted_string += f"({time_taken:.1f}s)\n" + formatted_string += f"({(time_taken[-1] / time_taken[0]):.1f}s)\n" if (current_placement + 1) % 10 == 0: formatted_string += "⎯⎯⎯⎯⎯⎯⎯⎯\n" - current_placement += 1 speed_embed = Embed( title="Average time taken to answer a question", @@ -61,9 +63,10 @@ class ScoreboardView(View): def _get_rank(self, member: Member) -> Embed: """Gets the member's rank for the points leaderboard and speed leaderboard.""" rank_embed = Embed(title=f"Ranks for {member.display_name}", color=Colours.python_blue) + # These are stored as strings so that the last digit can be determined to choose the suffix try: - points_rank = str(list(self.points.keys()).index(str(member.id)) + 1) - speed_rank = str(list(self.speed.keys()).index(str(member.id)) + 1) + points_rank = str(list(self.points.keys()).index(member.id) + 1) + speed_rank = str(list(self.speed.keys()).index(member.id) + 1) except ValueError: return Embed( title=choice(NEGATIVE_REPLIES), @@ -76,7 +79,7 @@ class ScoreboardView(View): name="Total Points", value=( f"You got {points_rank}{'th' if not (suffix := suffixes.get(points_rank[-1])) else suffix} place" - f" with {self.points[str(member.id)]} points." + f" with {self.points[member.id]} points." ), inline=False ) @@ -84,7 +87,7 @@ class ScoreboardView(View): name="Average Speed", value=( f"You got {speed_rank}{'th' if not (suffix := suffixes.get(speed_rank[-1])) else suffix} place" - f" with a time of {(self.speed[str(member.id)][1] / self.speed[str(member.id)][0]):.1f} seconds." + f" with a time of {(self.speed[member.id][1] / self.speed[member.id][0]):.1f} seconds." ), inline=False ) @@ -105,24 +108,17 @@ class Scoreboard: """Class for the scoreboard for the Trivia Night event.""" def __setitem__(self, key: str, value: int): - if key.startswith("points: "): - key = key.removeprefix("points: ") - if key not in self.view.points.keys(): - self.view.points[key] = value - else: - self.view.points[key] += self.view.points[key] - elif key.startswith("speed: "): - key = key.removeprefix("speed: ") - if key not in self.view.speed.keys(): - self.view.speed[key] = [1, value] - else: - self.view.speed[key] = [self.view.speed[key][0] + 1, self.view.speed[key][1] + value] - - def __getitem__(self, item: str): - if item.startswith("points: "): - return self.view.points[item.removeprefix("points: ")] - elif item.startswith("speed: "): - return self.view.speed[item.removeprefix("speed: ")] + if key.user_id not in self.view.points.keys(): + self.view.points[key.user_id] = value["points"] + else: + self.view.points[key.user_id] += self.view.points[key.user_id] + + if key.user_id not in self.view.speed.keys(): + self.view.speed[key.user_id] = [1, value["speed"]] + else: + self.view.speed[key.user_id] = [ + self.view.speed[key.user_id][0] + 1, self.view.speed[key.user_id][1] + value["speed"] + ] async def display(self) -> Union[Embed, View]: """Returns the embed of the main leaderboard along with the ScoreboardView.""" diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 2ec869ab..bb7c205b 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -1,4 +1,3 @@ -import logging from json import loads from random import choice @@ -7,6 +6,7 @@ from discord.ext import commands from bot.bot import Bot from bot.constants import Colours, POSITIVE_REPLIES + from ._questions import QuestionView, Questions from ._scoreboard import Scoreboard, ScoreboardView @@ -30,7 +30,6 @@ class TriviaNight(commands.Cog): json_text = (await ctx.message.attachments[0].read()).decode("utf8") serialized_json = loads(json_text) self.questions.view = QuestionView() - logging.getLogger(__name__).debug(self.questions.view) self.scoreboard.view = ScoreboardView(self.bot) self.questions.set_questions(serialized_json) success_embed = Embed( -- cgit v1.2.3 From f31eaf8f094f9d4572c9e6312fe3510a97441163 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 2 Nov 2021 18:07:17 -0400 Subject: adding stats to the correct answer embed --- bot/exts/events/trivianight/_questions.py | 38 ++++++++++++++++++++++++++---- bot/exts/events/trivianight/_scoreboard.py | 4 ++-- bot/exts/events/trivianight/trivianight.py | 10 ++++++++ 3 files changed, 45 insertions(+), 7 deletions(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 741e8422..f0a20521 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -32,13 +32,22 @@ class CurrentQuestion(TypedDict): class QuestionButton(Button): """Button subclass for the options of the questions.""" - def __init__(self, label: str, users_picked: dict): + def __init__(self, label: str, users_picked: dict, view: View): self.users_picked = users_picked + self._view = view super().__init__(label=label, style=discord.ButtonStyle.green) async def callback(self, interaction: Interaction) -> None: """When a user interacts with the button, this will be called.""" + 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] await interaction.response.send_message( embed=Embed( @@ -78,7 +87,7 @@ class QuestionView(View): super().__init__() self.current_question: CurrentQuestion self.users_picked = {} - self.buttons = [QuestionButton(label, self.users_picked) for label in ("A", "B", "C", "D")] + self.buttons = [QuestionButton(label, self.users_picked, self) for label in ("A", "B", "C", "D")] for button in self.buttons: self.add_item(button) @@ -92,6 +101,7 @@ class QuestionView(View): for label, answer in zip("ABCD", self.current_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 @@ -103,10 +113,15 @@ class QuestionView(View): labels = ("A", "B", "C", "D") label = labels[self.current_question["answers"].index(self.current_question["correct"])] return_dict = {name: info for name, info in self.users_picked.items() if info[0] == label} - self.users_picked = {} + all_players = list(self.users_picked.items()) + answers_chosen = { + answer_choice: len( + tuple(filter(lambda x: x[0] == answer_choice, self.users_picked.values())) + ) / len(all_players) + for answer_choice in "ABCD" + } - for button in self.buttons: - button.users_picked = self.users_picked + answers_chosen = dict(sorted(answers_chosen.items(), key=lambda item: item[1], reverse=True)) answer_embed = Embed( title=f"The correct answer for Question {self.current_question['number']} was..", @@ -114,6 +129,19 @@ class QuestionView(View): color=Colours.soft_green ) + for answer, percent in answers_chosen.items(): + # The `ord` function is used here to change the letter, say 'A' to its corresponding position in the answers + answer_embed.add_field( + name=f"{percent * 100:.1f}% of players chose", + value=self.current_question['answers'][ord(answer) - 65], + inline=False + ) + + self.users_picked = {} + + for button in self.buttons: + button.users_picked = self.users_picked + return return_dict, answer_embed diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index dbec966d..1bde59f5 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -26,7 +26,7 @@ class ScoreboardView(View): break user = await self.bot.fetch_user(int(user)) - formatted_string += f"`{current_placement + 1}`. {user.mention} " + formatted_string += f"**{current_placement + 1}.** {user.mention} " formatted_string += f"({points} pts)\n" if (current_placement + 1) % 10 == 0: formatted_string += "⎯⎯⎯⎯⎯⎯⎯⎯\n" @@ -48,7 +48,7 @@ class ScoreboardView(View): break user = await self.bot.fetch_user(int(user)) - formatted_string += f"`{current_placement + 1}`. {user.mention} " + formatted_string += f"**{current_placement + 1}.** {user.mention} " formatted_string += f"({(time_taken[-1] / time_taken[0]):.1f}s)\n" if (current_placement + 1) % 10 == 0: formatted_string += "⎯⎯⎯⎯⎯⎯⎯⎯\n" diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index bb7c205b..62b619e8 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -19,6 +19,14 @@ class TriviaNight(commands.Cog): self.scoreboard = Scoreboard() self.questions = Questions(self.scoreboard) + @staticmethod + def unicodeify(text: str) -> str: + """Takes `text` and adds zero-width spaces to prevent copy and pasting the question.""" + return "".join( + f"{letter}\u200b" if letter not in ('\n', '\t', '`', 'p', 'y') else letter + for idx, letter in enumerate(text) + ) + @commands.group() async def trivianight(self, ctx: commands.Context) -> None: """No-op subcommand group for organizing different commands.""" @@ -29,6 +37,8 @@ class TriviaNight(commands.Cog): """Load the JSON file provided into the questions.""" json_text = (await ctx.message.attachments[0].read()).decode("utf8") serialized_json = loads(json_text) + for idx, question in enumerate(serialized_json): + serialized_json[idx] = {**question, **{"description": self.unicodeify(question["description"])}} self.questions.view = QuestionView() self.scoreboard.view = ScoreboardView(self.bot) self.questions.set_questions(serialized_json) -- cgit v1.2.3 From adfddbd7c282e4361fc5d844068b952fae27eaed Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Thu, 4 Nov 2021 11:15:33 -0400 Subject: fixing list showing zero-width spaces --- bot/exts/events/trivianight/_questions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index f0a20521..6937d8ec 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -191,7 +191,8 @@ class Questions: spaces = len(sorted(self.questions, key=lambda question: len(question['description']))[-1]["description"]) + 3 formatted_string = "" for question in self.questions: - formatted_string += f"`Q{question['number']}: {question['description']!r}" \ + question_description = question['description'].replace("\u200b", "") + formatted_string += f"`Q{question['number']}: {question_description!r}" \ f"{' ' * (spaces - len(question['description']) + 2)}" \ f"|` {':x:' if not question.get('visited') else ':checkmark:'}\n" -- cgit v1.2.3 From 7ce200cbe8875baa7071abad4dcca1c7492bf366 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Thu, 4 Nov 2021 11:26:38 -0400 Subject: cog description for .trivianight, and allowing to pick questions that were already chosen --- bot/exts/events/trivianight/_questions.py | 13 +++++++++---- bot/exts/events/trivianight/trivianight.py | 17 ++++++++++++++--- 2 files changed, 23 insertions(+), 7 deletions(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 6937d8ec..8b4df74d 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -162,7 +162,7 @@ class Questions: If the number parameter is specified, it'll head to that specific question. """ - if all("visited" in question.keys() for question in self.questions): + 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.", @@ -179,7 +179,7 @@ class Questions: self.questions[question_number]["visited"] = True self.view.current_question = self.questions[question_number] - def list_questions(self) -> str: + def list_questions(self) -> Union[Embed, str]: """ Lists all questions from the question bank. @@ -188,11 +188,16 @@ class Questions: - 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'].replace("\u200b", "") - formatted_string += f"`Q{question['number']}: {question_description!r}" \ + formatted_string += f"`Q{question['number']}: {question['description']}" \ f"{' ' * (spaces - len(question['description']) + 2)}" \ f"|` {':x:' if not question.get('visited') else ':checkmark:'}\n" diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 62b619e8..37a29222 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -30,7 +30,15 @@ class TriviaNight(commands.Cog): @commands.group() async def trivianight(self, ctx: commands.Context) -> None: """No-op subcommand group for organizing different commands.""" - return + cog_description = Embed( + title="What is .trivianight?", + description=( + "This 'cog' is for the Python Discord's TriviaNight (date tentative)! Compete against other" + "players in a trivia about Python!" + ), + color=Colours.soft_green + ) + await ctx.send(embed=cog_description) @trivianight.command() async def load(self, ctx: commands.Context) -> None: @@ -88,8 +96,11 @@ class TriviaNight(commands.Cog): @trivianight.command() async def list(self, ctx: commands.Context) -> None: """Displays all the questions from the question bank.""" - formatted_string = self.questions.list_questions() - await ctx.send(formatted_string) + question_list = self.questions.list_questions() + if isinstance(question_list, Embed): + await ctx.send(embed=question_list) + + await ctx.send(question_list) @trivianight.command() async def stop(self, ctx: commands.Context) -> None: -- cgit v1.2.3 From 1848d0ddd318ff3eee0c9c9efed9fef89f460b21 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Thu, 4 Nov 2021 13:17:50 -0400 Subject: kahoot style scoring, time limits, and bug fixes --- bot/exts/events/trivianight/_questions.py | 59 +++++++++++++++++++----------- bot/exts/events/trivianight/_scoreboard.py | 2 +- bot/exts/events/trivianight/trivianight.py | 40 ++++++++++++++------ 3 files changed, 66 insertions(+), 35 deletions(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 8b4df74d..aaedf068 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -1,6 +1,6 @@ from random import choice, randrange from time import perf_counter -from typing import TypedDict, Union +from typing import Optional, TypedDict, Union import discord from discord import Embed, Interaction @@ -27,6 +27,8 @@ class CurrentQuestion(TypedDict): description: str answers: list[str] correct: str + points: Optional[int] + time: Optional[int] class QuestionButton(Button): @@ -106,22 +108,16 @@ class QuestionView(View): for button in self.buttons: button._time = current_time - return question_embed + time_limit = self.current_question.get("time", 10) + + return question_embed, time_limit def end_question(self) -> tuple[dict, Embed]: """Returns the dictionaries from the corresponding buttons for those who got it correct.""" labels = ("A", "B", "C", "D") label = labels[self.current_question["answers"].index(self.current_question["correct"])] - return_dict = {name: info for name, info in self.users_picked.items() if info[0] == label} + return_dict = {name: (*info, info[0] == label) for name, info in self.users_picked.items()} all_players = list(self.users_picked.items()) - answers_chosen = { - answer_choice: len( - tuple(filter(lambda x: x[0] == answer_choice, self.users_picked.values())) - ) / len(all_players) - for answer_choice in "ABCD" - } - - answers_chosen = dict(sorted(answers_chosen.items(), key=lambda item: item[1], reverse=True)) answer_embed = Embed( title=f"The correct answer for Question {self.current_question['number']} was..", @@ -129,20 +125,33 @@ class QuestionView(View): color=Colours.soft_green ) - for answer, percent in answers_chosen.items(): - # The `ord` function is used here to change the letter, say 'A' to its corresponding position in the answers - answer_embed.add_field( - name=f"{percent * 100:.1f}% of players chose", - value=self.current_question['answers'][ord(answer) - 65], - inline=False - ) + if len(all_players) != 0: + answers_chosen = { + answer_choice: len( + tuple(filter(lambda x: x[0] == answer_choice, self.users_picked.values())) + ) / len(all_players) + for answer_choice in "ABCD" + } + + answers_chosen = dict(sorted(answers_chosen.items(), key=lambda item: item[1], reverse=True)) + + for answer, percent in answers_chosen.items(): + # 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], + inline=False + ) self.users_picked = {} for button in self.buttons: button.users_picked = self.users_picked - return return_dict, answer_embed + time_limit = self.current_question.get("time", 10) + question_points = self.current_question.get("points", 10) + + return return_dict, answer_embed, time_limit, question_points class Questions: @@ -209,8 +218,14 @@ class Questions: def end_question(self) -> Embed: """Terminates answering of the question and displays the correct answer.""" - scores, answer_embed = self.view.end_question() + scores, answer_embed, time_limit, total_points = self.view.end_question() for user, score in scores.items(): - self.scoreboard[UserScore(user)] = {"points": 1, "speed": score[2]} - + # Overhead with calculating scores leads to inflated times, subtracts 0.5 to give an accurate depiction + time_taken = score[2] - 0.5 + 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 diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 1bde59f5..2adb5e37 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -27,7 +27,7 @@ class ScoreboardView(View): user = await self.bot.fetch_user(int(user)) formatted_string += f"**{current_placement + 1}.** {user.mention} " - formatted_string += f"({points} pts)\n" + formatted_string += f"({points:.1f} pts)\n" if (current_placement + 1) % 10 == 0: formatted_string += "⎯⎯⎯⎯⎯⎯⎯⎯\n" diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 37a29222..9973b6b1 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -1,3 +1,4 @@ +import asyncio from json import loads from random import choice @@ -27,18 +28,19 @@ class TriviaNight(commands.Cog): for idx, letter in enumerate(text) ) - @commands.group() + @commands.group(aliases=["tn"], invoke_without_command=True) async def trivianight(self, ctx: commands.Context) -> None: """No-op subcommand group for organizing different commands.""" - cog_description = Embed( - title="What is .trivianight?", - description=( - "This 'cog' is for the Python Discord's TriviaNight (date tentative)! Compete against other" - "players in a trivia about Python!" - ), - color=Colours.soft_green - ) - await ctx.send(embed=cog_description) + if ctx.invoked_subcommand is None: + cog_description = Embed( + title="What is .trivianight?", + description=( + "This 'cog' is for the Python Discord's TriviaNight (date tentative)! Compete against other" + "players in a trivia about Python!" + ), + color=Colours.soft_green + ) + await ctx.send(embed=cog_description) @trivianight.command() async def load(self, ctx: commands.Context) -> None: @@ -79,9 +81,16 @@ class TriviaNight(commands.Cog): await ctx.send(embed=next_question) return - question_embed, question_view = self.questions.current_question() + (question_embed, time_limit), question_view = self.questions.current_question() await ctx.send(embed=question_embed, view=question_view) + for time_remaining in range(time_limit - 1, -1, -1): + await asyncio.sleep(1) + if time_remaining % 5 == 0: + await ctx.send(f"{time_remaining}s remaining") + + await ctx.send(embed=self.questions.end_question()) + @trivianight.command() async def question(self, ctx: commands.Context, question_number: int) -> None: """Gets a question from the question bank depending on the question number provided.""" @@ -90,9 +99,16 @@ class TriviaNight(commands.Cog): await ctx.send(embed=question) return - question_embed, question_view = self.questions.current_question() + (question_embed, time_limit), question_view = self.questions.current_question() await ctx.send(embed=question_embed, view=question_view) + for time_remaining in range(time_limit - 1, -1, -1): + await asyncio.sleep(1) + if time_remaining % 5 == 0: + await ctx.send(f"{time_remaining}s remaining") + + await ctx.send(embed=self.questions.end_question()) + @trivianight.command() async def list(self, ctx: commands.Context) -> None: """Displays all the questions from the question bank.""" -- cgit v1.2.3 From a08e127f1535f63a24e785bfb1c16c445491303d Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Thu, 4 Nov 2021 16:02:37 -0400 Subject: bug fixes --- bot/exts/events/trivianight/_questions.py | 13 +++++- bot/exts/events/trivianight/_scoreboard.py | 6 +-- bot/exts/events/trivianight/trivianight.py | 75 ++++++++++++++++++++++++------ 3 files changed, 76 insertions(+), 18 deletions(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index aaedf068..8f2f5571 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -1,3 +1,4 @@ +import logging from random import choice, randrange from time import perf_counter from typing import Optional, TypedDict, Union @@ -10,6 +11,8 @@ from bot.constants import Colours, NEGATIVE_REPLIES from ._scoreboard import Scoreboard +logger = logging.getLogger(__name__) + class UserScore: """Marker class for passing into the scoreboard to add points/record speed.""" @@ -93,7 +96,9 @@ class QuestionView(View): for button in self.buttons: self.add_item(button) - def create_current_question(self) -> Embed: + self.active_question = False + + def create_current_question(self) -> Union[Embed, None]: """Helper function to create the embed for the current question.""" question_embed = Embed( title=f"Question {self.current_question['number']}", @@ -110,6 +115,8 @@ class QuestionView(View): time_limit = self.current_question.get("time", 10) + self.active_question = True + return question_embed, time_limit def end_question(self) -> tuple[dict, Embed]: @@ -151,6 +158,8 @@ class QuestionView(View): 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 @@ -183,7 +192,7 @@ class Questions: while "visited" in self.questions[question_number].keys(): question_number = randrange(0, len(self.questions)) else: - question_number = number + question_number = number - 1 self.questions[question_number]["visited"] = True self.view.current_question = self.questions[question_number] diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 2adb5e37..076fd406 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -79,7 +79,7 @@ class ScoreboardView(View): name="Total Points", value=( f"You got {points_rank}{'th' if not (suffix := suffixes.get(points_rank[-1])) else suffix} place" - f" with {self.points[member.id]} points." + f" with {self.points[member.id]:.1f} points." ), inline=False ) @@ -108,12 +108,12 @@ class Scoreboard: """Class for the scoreboard for the Trivia Night event.""" def __setitem__(self, key: str, value: int): - if key.user_id not in self.view.points.keys(): + if value.get("points") and key.user_id not in self.view.points.keys(): self.view.points[key.user_id] = value["points"] else: self.view.points[key.user_id] += self.view.points[key.user_id] - if key.user_id not in self.view.speed.keys(): + if value.get("speed") and key.user_id not in self.view.speed.keys(): self.view.speed[key.user_id] = [1, value["speed"]] else: self.view.speed[key.user_id] = [ diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 9973b6b1..46db8c74 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -1,12 +1,13 @@ import asyncio -from json import loads +from json import JSONDecodeError, loads from random import choice +from typing import Optional from discord import Embed from discord.ext import commands from bot.bot import Bot -from bot.constants import Colours, POSITIVE_REPLIES +from bot.constants import Colours, NEGATIVE_REPLIES, POSITIVE_REPLIES from ._questions import QuestionView, Questions from ._scoreboard import Scoreboard, ScoreboardView @@ -43,10 +44,27 @@ class TriviaNight(commands.Cog): await ctx.send(embed=cog_description) @trivianight.command() - async def load(self, ctx: commands.Context) -> None: + async def load(self, ctx: commands.Context, *, to_load: Optional[str]) -> None: """Load the JSON file provided into the questions.""" - json_text = (await ctx.message.attachments[0].read()).decode("utf8") - serialized_json = loads(json_text) + if ctx.message.attachments: + json_text = (await ctx.message.attachments[0].read()).decode("utf8") + elif to_load.startswith("https://discord.com/channels") or \ + to_load.startswith("https://discordapp.com/channels"): + channel_id, message_id = to_load.split("/")[-2:] + channel = await ctx.guild.fetch_channel(int(channel_id)) + message = await channel.fetch_message(int(message_id)) + if message.attachments: + json_text = (await message.attachments[0].read()).decode("utf8") + else: + json_text = message.content.replace("```", "").replace("json", "") + else: + json_text = message.content.replace("```", "").replace("json", "") + + try: + serialized_json = loads(json_text) + except JSONDecodeError: + raise commands.BadArgument("Invalid JSON") + for idx, question in enumerate(serialized_json): serialized_json[idx] = {**question, **{"description": self.unicodeify(question["description"])}} self.questions.view = QuestionView() @@ -64,7 +82,8 @@ class TriviaNight(commands.Cog): """Resets previous questions and scoreboards.""" self.scoreboard.view = ScoreboardView(self.bot) for question in self.questions.questions: - del question["visited"] + if "visited" in question.keys(): + del question["visited"] success_embed = Embed( title=choice(POSITIVE_REPLIES), @@ -76,20 +95,35 @@ class TriviaNight(commands.Cog): @trivianight.command() async def next(self, ctx: commands.Context) -> None: """Gets a random question from the unanswered question list and lets user choose the answer.""" + if self.questions.view.active_question is True: + error_embed = Embed( + title=choice(NEGATIVE_REPLIES), + description="There is already an ongoing question!", + color=Colours.soft_red + ) + await ctx.send(embed=error_embed) + return + next_question = self.questions.next_question() if isinstance(next_question, Embed): await ctx.send(embed=next_question) return (question_embed, time_limit), question_view = self.questions.current_question() - await ctx.send(embed=question_embed, view=question_view) + message = await ctx.send(embed=question_embed, view=question_view) + + for time_remaining in range(time_limit, -1, -1): + if self.questions.view.active_question is False: + await ctx.send(embed=self.questions.end_question()) + await message.edit(embed=question_embed, view=None) + return - for time_remaining in range(time_limit - 1, -1, -1): await asyncio.sleep(1) - if time_remaining % 5 == 0: + if time_remaining % 5 == 0 and time_remaining not in (time_limit, 0): await ctx.send(f"{time_remaining}s remaining") await ctx.send(embed=self.questions.end_question()) + await message.edit(embed=question_embed, view=None) @trivianight.command() async def question(self, ctx: commands.Context, question_number: int) -> None: @@ -100,14 +134,20 @@ class TriviaNight(commands.Cog): return (question_embed, time_limit), question_view = self.questions.current_question() - await ctx.send(embed=question_embed, view=question_view) + message = await ctx.send(embed=question_embed, view=question_view) + + for time_remaining in range(time_limit, -1, -1): + if self.questions.view.active_question is False: + await ctx.send(embed=self.questions.end_question()) + await message.edit(embed=question_embed, view=None) + return - for time_remaining in range(time_limit - 1, -1, -1): await asyncio.sleep(1) - if time_remaining % 5 == 0: + if time_remaining % 5 == 0 and time_remaining not in (time_limit, 0): await ctx.send(f"{time_remaining}s remaining") await ctx.send(embed=self.questions.end_question()) + await message.edit(embed=question_embed, view=None) @trivianight.command() async def list(self, ctx: commands.Context) -> None: @@ -121,7 +161,16 @@ class TriviaNight(commands.Cog): @trivianight.command() async def stop(self, ctx: commands.Context) -> None: """End the ongoing question to show the correct question.""" - await ctx.send(embed=self.questions.end_question()) + if self.questions.view.active_question is False: + error_embed = Embed( + title=choice(NEGATIVE_REPLIES), + description="There is not an ongoing question to stop!", + color=Colours.soft_red + ) + await ctx.send(embed=error_embed) + return + + self.questions.view.active_question = False @trivianight.command() async def end(self, ctx: commands.Context) -> None: -- cgit v1.2.3 From 00dcdee6d5b4fbbb763af6138974337e04421a5d Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Thu, 4 Nov 2021 16:04:05 -0400 Subject: change checkmark to white_check_mark --- bot/exts/events/trivianight/_questions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 8f2f5571..f433baa8 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -217,7 +217,7 @@ class Questions: for question in self.questions: formatted_string += f"`Q{question['number']}: {question['description']}" \ f"{' ' * (spaces - len(question['description']) + 2)}" \ - f"|` {':x:' if not question.get('visited') else ':checkmark:'}\n" + f"|` {':x:' if not question.get('visited') else ':white_check_mark:'}\n" return formatted_string.strip() -- cgit v1.2.3 From ec00a842b958edf0100e197ae29f52a9a33d6a6b Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Thu, 4 Nov 2021 16:13:20 -0400 Subject: fixing rivianight list formatting --- bot/exts/events/trivianight/_questions.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index f433baa8..53f0a4e0 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -212,11 +212,16 @@ class Questions: 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 + spaces = len( + sorted( + self.questions, key=lambda question: len(question['description'].replace("\u200b", "")) + )[-1]["description"].replace("\u200b", "") + ) + 3 formatted_string = "" for question in self.questions: - formatted_string += f"`Q{question['number']}: {question['description']}" \ - f"{' ' * (spaces - len(question['description']) + 2)}" \ + question_description = question["description"].replace("\u200b", "") + 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() -- cgit v1.2.3 From 1e44a974ed1946c424b61c43d3fbc5ef23e37613 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 16 Nov 2021 17:39:09 -0500 Subject: feat: allow a dynamic number of questions, not just being 4 answers --- bot/exts/events/trivianight/_questions.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 53f0a4e0..f94371da 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -92,20 +92,22 @@ class QuestionView(View): super().__init__() self.current_question: CurrentQuestion self.users_picked = {} - self.buttons = [QuestionButton(label, self.users_picked, self) for label in ("A", "B", "C", "D")] - for button in self.buttons: - self.add_item(button) self.active_question = False def create_current_question(self) -> Union[Embed, None]: """Helper function to create the embed for the current question.""" + self.current_labels = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[:len(self.current_question["answers"])] question_embed = Embed( title=f"Question {self.current_question['number']}", description=self.current_question["description"], color=Colours.python_yellow ) - for label, answer in zip("ABCD", self.current_question["answers"]): + 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"]): question_embed.add_field(name=f"Answer {label}", value=answer, inline=False) question_embed.set_footer(text="0 people have answered") @@ -121,7 +123,7 @@ class QuestionView(View): def end_question(self) -> tuple[dict, Embed]: """Returns the dictionaries from the corresponding buttons for those who got it correct.""" - labels = ("A", "B", "C", "D") + 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()) @@ -137,7 +139,7 @@ class QuestionView(View): answer_choice: len( tuple(filter(lambda x: x[0] == answer_choice, self.users_picked.values())) ) / len(all_players) - for answer_choice in "ABCD" + for answer_choice in labels } answers_chosen = dict(sorted(answers_chosen.items(), key=lambda item: item[1], reverse=True)) -- cgit v1.2.3 From 8098e98e6b7fa8e214b78a232c5c37d8e24dfe6b Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 16 Nov 2021 18:26:02 -0500 Subject: map percentage of people who got it correct to color --- bot/exts/events/trivianight/_questions.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index f94371da..9f2b20da 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -128,10 +128,14 @@ class QuestionView(View): return_dict = {name: (*info, info[0] == label) for name, info in self.users_picked.items()} all_players = list(self.users_picked.items()) + # Maps the % of people who got it right to a color, from a range of red to green + percentage_to_color = { + range(0, 26): 0xFC94A1, range(26, 51): 0xFFCCCB, range(51, 76): 0xCDFFCC, range(76, 101): 0xB0F5AB + } + answer_embed = Embed( title=f"The correct answer for Question {self.current_question['number']} was..", - description=self.current_question["correct"], - color=Colours.soft_green + description=self.current_question["correct"] ) if len(all_players) != 0: @@ -144,7 +148,12 @@ class QuestionView(View): answers_chosen = dict(sorted(answers_chosen.items(), key=lambda item: item[1], reverse=True)) - for answer, percent in answers_chosen.items(): + for idx, (answer, percent) in enumerate(answers_chosen.items()): + # Setting the color of answer_embed to the % of people that got it correct via the mapping + if idx == 0: + all_ranges = [range(0, 26), range(26, 51), range(51, 76), range(76, 101)] + answer_embed.color = percentage_to_color[all_ranges[round(percent * 100) // 25 - 1]] + # 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", -- cgit v1.2.3 From 07ac7f87610d513342e9393c29ea49c67cb76215 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Wed, 17 Nov 2021 17:47:09 -0500 Subject: bluenix review changes --- bot/exts/events/trivianight/__init__.py | 7 ++++++ bot/exts/events/trivianight/_questions.py | 38 +++++++++--------------------- bot/exts/events/trivianight/_scoreboard.py | 7 +++--- bot/exts/events/trivianight/trivianight.py | 7 +++--- 4 files changed, 26 insertions(+), 33 deletions(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/__init__.py b/bot/exts/events/trivianight/__init__.py index e69de29b..87de18e0 100644 --- a/bot/exts/events/trivianight/__init__.py +++ b/bot/exts/events/trivianight/__init__.py @@ -0,0 +1,7 @@ +class UserScore: + """Marker class for passing into the scoreboard to add points/record speed.""" + + __slots__ = ("user_id",) + + def __init__(self, user_id: int): + self.user_id = user_id diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 9f2b20da..2bbff1d7 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -1,4 +1,3 @@ -import logging from random import choice, randrange from time import perf_counter from typing import Optional, TypedDict, Union @@ -9,19 +8,9 @@ from discord.ui import Button, View from bot.constants import Colours, NEGATIVE_REPLIES +from . import UserScore from ._scoreboard import Scoreboard -logger = logging.getLogger(__name__) - - -class UserScore: - """Marker class for passing into the scoreboard to add points/record speed.""" - - __slots__ = ("user_id",) - - def __init__(self, user_id: int): - self.user_id = user_id - class CurrentQuestion(TypedDict): """Representing the different 'keys' of the question taken from the JSON.""" @@ -100,7 +89,7 @@ class QuestionView(View): self.current_labels = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[:len(self.current_question["answers"])] question_embed = Embed( title=f"Question {self.current_question['number']}", - description=self.current_question["description"], + description=self.current_question["obfuscated_description"], color=Colours.python_yellow ) self.buttons = [QuestionButton(label, self.users_picked, self) for label in self.current_labels] @@ -128,11 +117,6 @@ class QuestionView(View): return_dict = {name: (*info, info[0] == label) for name, info in self.users_picked.items()} all_players = list(self.users_picked.items()) - # Maps the % of people who got it right to a color, from a range of red to green - percentage_to_color = { - range(0, 26): 0xFC94A1, range(26, 51): 0xFFCCCB, range(51, 76): 0xCDFFCC, range(76, 101): 0xB0F5AB - } - answer_embed = Embed( title=f"The correct answer for Question {self.current_question['number']} was..", description=self.current_question["correct"] @@ -146,13 +130,12 @@ class QuestionView(View): for answer_choice in labels } - answers_chosen = dict(sorted(answers_chosen.items(), key=lambda item: item[1], reverse=True)) - for idx, (answer, percent) in enumerate(answers_chosen.items()): # Setting the color of answer_embed to the % of people that got it correct via the mapping if idx == 0: - all_ranges = [range(0, 26), range(26, 51), range(51, 76), range(76, 101)] - answer_embed.color = percentage_to_color[all_ranges[round(percent * 100) // 25 - 1]] + # Maps the % of people who got it right to a color, from a range of red to green + percentage_to_color = [0xFC94A1, 0xFFCCCB, 0xCDFFCC, 0xB0F5AB, 0xB0F5AB] + answer_embed.color = percentage_to_color[round(percent * 100) // 25] # The `ord` function is used here to change the letter to its corresponding position answer_embed.add_field( @@ -206,6 +189,8 @@ class Questions: question_number = number - 1 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]: @@ -225,12 +210,12 @@ class Questions: ) spaces = len( sorted( - self.questions, key=lambda question: len(question['description'].replace("\u200b", "")) - )[-1]["description"].replace("\u200b", "") + self.questions, key=lambda question: len(question['description']) + )[-1]["description"] ) + 3 formatted_string = "" for question in self.questions: - question_description = question["description"].replace("\u200b", "") + 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" @@ -245,8 +230,7 @@ class Questions: """Terminates answering of the question and displays the correct answer.""" scores, answer_embed, time_limit, total_points = self.view.end_question() for user, score in scores.items(): - # Overhead with calculating scores leads to inflated times, subtracts 0.5 to give an accurate depiction - time_taken = score[2] - 0.5 + 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} diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 08025214..635660a2 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -1,5 +1,4 @@ from random import choice -from typing import Union import discord.ui from discord import ButtonStyle, Embed, Interaction, Member @@ -8,6 +7,8 @@ from discord.ui import Button, View from bot.bot import Bot from bot.constants import Colours, NEGATIVE_REPLIES +from . import UserScore + class ScoreboardView(View): """View for the scoreboard.""" @@ -110,7 +111,7 @@ class ScoreboardView(View): class Scoreboard: """Class for the scoreboard for the Trivia Night event.""" - def __setitem__(self, key: str, value: int): + def __setitem__(self, key: UserScore, value: dict): if value.get("points") and key.user_id not in self.view.points.keys(): self.view.points[key.user_id] = value["points"] elif value.get("points"): @@ -123,6 +124,6 @@ class Scoreboard: self.view.speed[key.user_id][0] + 1, self.view.speed[key.user_id][1] + value["speed"] ] - async def display(self) -> Union[Embed, View]: + async def display(self) -> tuple[Embed, View]: """Returns the embed of the main leaderboard along with the ScoreboardView.""" return await self.view.create_main_leaderboard(), self.view diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 224b0620..615a9dd3 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -51,7 +51,7 @@ class TriviaNight(commands.Cog): The JSON provided is formatted where it is a list of dictionaries, each dictionary containing the keys below: - number: int (represents the current question #) - description: str (represents the question itself) - - answers: list (represents the different answers possible, must be a length of 4) + - answers: list[str] (represents the different answers possible, must be a length of 4) - correct: str (represents the correct answer in terms of what the correct answer is in `answers` - time: Optional[int] (represents the timer for the question and how long it should run, default is 10) - points: Optional[int] (represents how many points are awarded for each question, default is 10) @@ -79,7 +79,8 @@ class TriviaNight(commands.Cog): raise commands.BadArgument("Invalid JSON") for idx, question in enumerate(serialized_json): - serialized_json[idx] = {**question, **{"description": self.unicodeify(question["description"])}} + serialized_json[idx]["obfuscated_description"] = self.unicodeify(question["description"]) + self.questions.view = QuestionView() self.scoreboard.view = ScoreboardView(self.bot) self.questions.set_questions(serialized_json) @@ -101,7 +102,7 @@ class TriviaNight(commands.Cog): if "visited" in question.keys(): del question["visited"] - self.questions.questions = list(all_questions) + self.questions.set_questions(list(all_questions)) success_embed = Embed( title=choice(POSITIVE_REPLIES), -- cgit v1.2.3 From 49c9cc470e0fac075140f8f7938a5fb140b7ff0c Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 28 Nov 2021 22:44:34 -0500 Subject: making the docstrings nicer --- bot/exts/events/trivianight/_questions.py | 48 ++++++++++++++++++++++---- bot/exts/events/trivianight/_scoreboard.py | 41 +++++++++++++++++++--- bot/exts/events/trivianight/trivianight.py | 55 ++++++++++++++++++++++++++---- 3 files changed, 125 insertions(+), 19 deletions(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 2bbff1d7..eaabed4f 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -32,7 +32,13 @@ class QuestionButton(Button): super().__init__(label=label, style=discord.ButtonStyle.green) async def callback(self, interaction: Interaction) -> None: - """When a user interacts with the button, this will be called.""" + """ + When a user interacts with the button, this will be called. + + Parameters: + - 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] @@ -84,8 +90,13 @@ class QuestionView(View): self.active_question = False - def create_current_question(self) -> Union[Embed, None]: - """Helper function to create the embed for the current question.""" + def create_current_question(self) -> tuple[Embed, int]: + """ + Helper function to create the embed for the current 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. + """ self.current_labels = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[:len(self.current_question["answers"])] question_embed = Embed( title=f"Question {self.current_question['number']}", @@ -110,8 +121,16 @@ class QuestionView(View): return question_embed, time_limit - def end_question(self) -> tuple[dict, Embed]: - """Returns the dictionaries from the corresponding buttons for those who got it correct.""" + def end_question(self) -> tuple[dict, Embed, int, int]: + """ + 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 + """ 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()} @@ -165,7 +184,13 @@ class Questions: self.questions = [] def set_questions(self, questions: list) -> None: - """Setting `self.questions` dynamically via a function to set it.""" + """ + 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]: @@ -173,6 +198,9 @@ class Questions: 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( @@ -227,7 +255,13 @@ class Questions: return self.view.create_current_question(), self.view def end_question(self) -> Embed: - """Terminates answering of the question and displays the correct answer.""" + """ + 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] diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 635660a2..40f93475 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -20,7 +20,13 @@ class ScoreboardView(View): self.speed = {} async def create_main_leaderboard(self) -> Embed: - """Helper function that iterates through `self.points` to generate the main leaderboard embed.""" + """ + Helper function that iterates through `self.points` to generate the main leaderboard embed. + + The main leaderboard would be formatted like the following: + **1**. @mention of the user (# of points) + along with the 29 other users who made it onto the leaderboard. + """ formatted_string = "" self.points = dict(sorted(self.points.items(), key=lambda item: item[-1], reverse=True)) self.speed = dict(sorted(self.speed.items(), key=lambda item: item[-1])) @@ -44,7 +50,13 @@ class ScoreboardView(View): return main_embed async def _create_speed_embed(self) -> Embed: - """Helper function that iterates through `self.speed` to generate a leaderboard embed.""" + """ + Helper function that iterates through `self.speed` to generate a leaderboard embed. + + The speed leaderboard would be formatted like the following: + **1**. @mention of the user ([average speed as a float with the precision of one decimal point]s) + along with the 29 other users who made it onto the leaderboard. + """ formatted_string = "" for current_placement, (user, time_taken) in enumerate(self.speed.items()): @@ -65,7 +77,12 @@ class ScoreboardView(View): return speed_embed def _get_rank(self, member: Member) -> Embed: - """Gets the member's rank for the points leaderboard and speed leaderboard.""" + """ + Gets the member's rank for the points leaderboard and speed leaderboard. + + Parameters: + - member: An instance of discord.Member representing the person who is trying to get their rank. + """ rank_embed = Embed(title=f"Ranks for {member.display_name}", color=Colours.python_blue) # These are stored as strings so that the last digit can be determined to choose the suffix try: @@ -99,12 +116,26 @@ class ScoreboardView(View): @discord.ui.button(label="Scoreboard for Speed", style=ButtonStyle.green) async def speed_leaderboard(self, button: Button, interaction: Interaction) -> None: - """Send an ephemeral message with the speed leaderboard embed.""" + """ + Send an ephemeral message with the speed leaderboard embed. + + Parameters: + - button: The discord.ui.Button instance representing the `Speed Leaderboard` button. + - interaction: The discord.Interaction instance containing information on the interaction between the user + and the button. + """ await interaction.response.send_message(embed=await self._create_speed_embed(), ephemeral=True) @discord.ui.button(label="What's my rank?", style=ButtonStyle.blurple) async def rank_button(self, button: Button, interaction: Interaction) -> None: - """Send an ephemeral message with the user's rank for the overall points/average speed.""" + """ + Send an ephemeral message with the user's rank for the overall points/average speed. + + Parameters: + - button: The discord.ui.Button instance representing the `What's my rank?` button. + - interaction: The discord.Interaction instance containing information on the interaction between the user + and the button. + """ await interaction.response.send_message(embed=self._get_rank(interaction.user), ephemeral=True) diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index ed2bfdbe..a86bd73f 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -37,7 +37,12 @@ class TriviaNight(commands.Cog): @staticmethod def unicodeify(text: str) -> str: - """Takes `text` and adds zero-width spaces to prevent copy and pasting the question.""" + """ + Takes `text` and adds zero-width spaces to prevent copy and pasting the question. + + Parameters: + - text: A string that represents the question description to 'unicodeify' + """ return "".join( f"{letter}\u200b" if letter not in ('\n', '\t', '`', 'p', 'y') else letter for idx, letter in enumerate(text) @@ -45,7 +50,11 @@ class TriviaNight(commands.Cog): @commands.group(aliases=["tn"], invoke_without_command=True) async def trivianight(self, ctx: commands.Context) -> None: - """No-op subcommand group for organizing different commands.""" + """ + The command group for the Python Discord Trivia Night. + + If invoked without a subcommand (i.e. simply .trivianight), it will explain what the Trivia Night event is. + """ cog_description = Embed( title="What is .trivianight?", description=( @@ -88,6 +97,8 @@ class TriviaNight(commands.Cog): json_text = (await message.attachments[0].read()).decode("utf8") else: json_text = message.content.replace("```", "").replace("json", "").replace("\n", "") + else: + json_text = to_load.replace("```", "").replace("json", "").replace("\n", "") try: serialized_json = loads(json_text) @@ -128,7 +139,12 @@ class TriviaNight(commands.Cog): @trivianight.command() @commands.has_any_role(*TRIVIA_NIGHT_ROLES) async def next(self, ctx: commands.Context) -> None: - """Gets a random question from the unanswered question list and lets user choose the answer.""" + """ + Gets a random question from the unanswered question list and lets the user(s) choose the answer. + + This command will continuously count down until the time limit of the question is exhausted. + However, if `.trivianight stop` is invoked, the counting down is interrupted to show the final results. + """ if self.questions.view.active_question is True: error_embed = Embed( title=choice(NEGATIVE_REPLIES), @@ -162,7 +178,15 @@ class TriviaNight(commands.Cog): @trivianight.command() @commands.has_any_role(*TRIVIA_NIGHT_ROLES) async def question(self, ctx: commands.Context, question_number: int) -> None: - """Gets a question from the question bank depending on the question number provided.""" + """ + Gets a question from the question bank depending on the question number provided. + + The logic of this command is similar to `.trivianight next`, with the only difference being that you need to + specify the question number. + + Parameters: + - question_number: An integer represents the question number to go to (i.e. .trivianight question 5). + """ question = self.questions.next_question(question_number) if isinstance(question, Embed): await ctx.send(embed=question) @@ -187,7 +211,12 @@ class TriviaNight(commands.Cog): @trivianight.command() @commands.has_any_role(*TRIVIA_NIGHT_ROLES) async def list(self, ctx: commands.Context) -> None: - """Displays all the questions from the question bank.""" + """ + Displays all the questions from the question bank. + + Questions are displayed in the following format: + Q(number): Question description | :white_check_mark: if the question was used otherwise :x:. + """ question_list = self.questions.list_questions() if isinstance(question_list, Embed): await ctx.send(embed=question_list) @@ -197,7 +226,11 @@ class TriviaNight(commands.Cog): @trivianight.command() @commands.has_any_role(*TRIVIA_NIGHT_ROLES) async def stop(self, ctx: commands.Context) -> None: - """End the ongoing question to show the correct question.""" + """ + End the ongoing question to show the correct question. + + This command should be used if the question should be ended early or if the time limit fails + """ if self.questions.view.active_question is False: error_embed = Embed( title=choice(NEGATIVE_REPLIES), @@ -212,7 +245,15 @@ class TriviaNight(commands.Cog): @trivianight.command() @commands.has_any_role(*TRIVIA_NIGHT_ROLES) async def end(self, ctx: commands.Context) -> None: - """Ends the trivia night event and displays the scoreboard.""" + """ + Ends the trivia night event and displays the scoreboard view. + + The scoreboard view consists of the two scoreboards with the 30 players who got the highest points and the + 30 players who had the fastest average response time to a question where they got the question right. + + The scoreboard view also has a button where the user can see their own rank, points and average speed if they + didn't make it onto the leaderboard. + """ scoreboard_embed, scoreboard_view = await self.scoreboard.display() await ctx.send(embed=scoreboard_embed, view=scoreboard_view) -- cgit v1.2.3 From f821d802357193f82723233f5dd1d55d51ec8ea6 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 29 Nov 2021 19:40:28 -0500 Subject: prevent bugs with question regarding multiple choices --- bot/exts/events/trivianight/_questions.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index eaabed4f..0ab657d2 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -103,9 +103,10 @@ class QuestionView(View): description=self.current_question["obfuscated_description"], color=Colours.python_yellow ) - self.buttons = [QuestionButton(label, self.users_picked, self) for label in self.current_labels] - for button in self.buttons: - self.add_item(button) + 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"]): question_embed.add_field(name=f"Answer {label}", value=answer, inline=False) @@ -216,6 +217,8 @@ class 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 -- cgit v1.2.3 From 33269228576ffb7d48187fd9cd0297d8b0b657e3 Mon Sep 17 00:00:00 2001 From: Bluenix Date: Sun, 12 Dec 2021 22:30:44 +0100 Subject: Restructure trivia night game around new Question representation --- bot/exts/events/trivianight/_game.py | 60 ++++++- bot/exts/events/trivianight/_questions.py | 253 ++++++++--------------------- bot/exts/events/trivianight/trivianight.py | 161 +++++++----------- 3 files changed, 179 insertions(+), 295 deletions(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index 086d0de6..aac745a7 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -1,4 +1,5 @@ import time +from random import randrange from string import ascii_uppercase from typing import Iterable, Optional, TypedDict @@ -25,6 +26,14 @@ UserGuess = tuple[ ] +class QuestionClosed(RuntimeError): + """Exception raised when the question is not open for guesses anymore.""" + + +class AlreadyUpdated(RuntimeError): + """Exception raised when the user has already updated their guess once.""" + + class Question: """Interface for one question in a trivia night game.""" @@ -76,10 +85,10 @@ class Question: def _update_guess(self, user: int, answer: str) -> UserGuess: """Update an already existing guess.""" if self._started is None: - raise RuntimeError("Question is not open for answers.") + raise QuestionClosed("Question is not open for answers.") if self._guesses[user][1] is False: - raise RuntimeError(f"User({user}) has already updated their guess once.") + raise AlreadyUpdated(f"User({user}) has already updated their guess once.") self._guesses[user] = (answer, False, time.perf_counter() - self._started) return self._guesses[user] @@ -90,7 +99,7 @@ class Question: return self._update_guess(user, answer) if self._started is None: - raise RuntimeError("Question is not open for answers.") + raise QuestionClosed("Question is not open for answers.") self._guesses[user] = (answer, True, time.perf_counter() - self._started) return self._guesses[user] @@ -103,3 +112,48 @@ class Question: self._guesses = {} return guesses + + +class TriviaNightGame: + """Interface for managing a game of trivia night.""" + + def __init__(self, data: list[QuestionData]) -> None: + self._questions = [Question(q) for q in data] + self.current_question: Optional[Question] = None + + def __iter__(self) -> Iterable[Question]: + return iter(self._questions) + + def next_question(self, number: str = None) -> Question: + """ + Consume one random question from the trivia night game. + + One question is randomly picked from the list of questions which is then removed and returned. + """ + if self.current_question is not None: + raise RuntimeError("Cannot call next_question() when there is a current question.") + + if number is not None: + try: + question = [q for q in self._questions if q.number == number][0] + except IndexError: + raise ValueError(f"Question number {number} does not exist.") + else: + question = self._questions.pop(randrange(len(self._questions))) + + self.current_question = question + self.current_question.start() + return question + + def end_question(self) -> None: + """ + End the current question. + + This method should be called when the question has been answered, it must be called before + attempting to call `next_question()` again. + """ + if self.current_question is None: + raise RuntimeError("Cannot call end_question() when there is no current question.") + + self.current_question.stop() + self.current_question = None 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 diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 59b809b1..86da0c3a 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -4,11 +4,13 @@ from random import choice from typing import Optional from discord import Embed +from discord.colour import Color from discord.ext import commands from bot.bot import Bot from bot.constants import Colours, NEGATIVE_REPLIES, POSITIVE_REPLIES, Roles +from ._game import TriviaNightGame from ._questions import QuestionView, Questions from ._scoreboard import Scoreboard, ScoreboardView @@ -16,37 +18,12 @@ from ._scoreboard import Scoreboard, ScoreboardView TRIVIA_NIGHT_ROLES = (Roles.admin, 78361735739998228) -class TriviaNight(commands.Cog): +class TriviaNightCog(commands.Cog): """Cog for the Python Trivia Night event.""" def __init__(self, bot: Bot): self.bot = bot - self.scoreboard = Scoreboard() - self.questions = Questions(self.scoreboard) - - def setup_views(self, questions: dict) -> None: - """ - Sets up the views for `self.questions` and `self.scoreboard` respectively. - - Parameters: - - questions: The dictionary to set the questions for self.questions to use. - """ - self.questions.view = QuestionView() - self.scoreboard.view = ScoreboardView(self.bot) - self.questions.set_questions(questions) - - @staticmethod - def unicodeify(text: str) -> str: - """ - Takes `text` and adds zero-width spaces to prevent copy and pasting the question. - - Parameters: - - text: A string that represents the question description to 'unicodeify' - """ - return "".join( - f"{letter}\u200b" if letter not in ('\n', '\t', '`', 'p', 'y') else letter - for idx, letter in enumerate(text) - ) + self.game: Optional[TriviaNightGame] = None @commands.group(aliases=["tn"], invoke_without_command=True) async def trivianight(self, ctx: commands.Context) -> None: @@ -84,6 +61,14 @@ class TriviaNight(commands.Cog): - a message link to the attachment/JSON - reading the JSON itself via a codeblock or plain text """ + if self.game is not None: + await ctx.send(embed=Embed( + title=choice(NEGATIVE_REPLIES), + description="There is already a trivia night running!", + color=Colours.soft_red + )) + return + if ctx.message.attachments: json_text = (await ctx.message.attachments[0].read()).decode("utf8") elif not to_load: @@ -105,10 +90,7 @@ class TriviaNight(commands.Cog): except JSONDecodeError: raise commands.BadArgument("Invalid JSON") - for idx, question in enumerate(serialized_json): - serialized_json[idx]["obfuscated_description"] = self.unicodeify(question["description"]) - - self.setup_views(serialized_json) + self.game = TriviaNightGame(serialized_json) success_embed = Embed( title=choice(POSITIVE_REPLIES), @@ -117,35 +99,24 @@ class TriviaNight(commands.Cog): ) await ctx.send(embed=success_embed) - @trivianight.command() - @commands.has_any_role(*TRIVIA_NIGHT_ROLES) - async def reset(self, ctx: commands.Context) -> None: - """Resets previous questions and scoreboards.""" - all_questions = self.questions.questions - - for question in all_questions: - if "visited" in question.keys(): - del question["visited"] - - self.setup_views(list(all_questions)) - - success_embed = Embed( - title=choice(POSITIVE_REPLIES), - description="The scoreboards were reset and questions reset!", - color=Colours.soft_green - ) - await ctx.send(embed=success_embed) - - @trivianight.command() + @trivianight.command(aliases=('next',)) @commands.has_any_role(*TRIVIA_NIGHT_ROLES) - async def next(self, ctx: commands.Context) -> None: + async def question(self, ctx: commands.Context, question_number: str = None) -> None: """ Gets a random question from the unanswered question list and lets the user(s) choose the answer. This command will continuously count down until the time limit of the question is exhausted. However, if `.trivianight stop` is invoked, the counting down is interrupted to show the final results. """ - if self.questions.view.active_question is True: + if self.game is None: + await ctx.send(embed=Embed( + title=choice(NEGATIVE_REPLIES), + description="There is no trivia night running!", + color=Colours.soft_red + )) + return + + if self.game.current_question is not None: error_embed = Embed( title=choice(NEGATIVE_REPLIES), description="There is already an ongoing question!", @@ -154,65 +125,40 @@ class TriviaNight(commands.Cog): await ctx.send(embed=error_embed) return - next_question = self.questions.next_question() - if isinstance(next_question, Embed): - await ctx.send(embed=next_question) - return + next_question = self.game.next_question(question_number) - (question_embed, time_limit), question_view = self.questions.current_question() - message = await ctx.send(embed=question_embed, view=question_view) + question_view = QuestionView(next_question) + question_embed = question_view.create_embed() - for time_remaining in range(time_limit, -1, -1): - if self.questions.view.active_question is False: - await ctx.send(embed=self.questions.end_question()) - await message.edit(embed=question_embed, view=None) - return - - await asyncio.sleep(1) - if time_remaining % 5 == 0 and time_remaining not in (time_limit, 0): - await ctx.send(f"{time_remaining}s remaining") - - await ctx.send(embed=self.questions.end_question()) - await message.edit(embed=question_embed, view=None) - - @trivianight.command() - @commands.has_any_role(*TRIVIA_NIGHT_ROLES) - async def question(self, ctx: commands.Context, question_number: int) -> None: - """ - Gets a question from the question bank depending on the question number provided. - - The logic of this command is similar to `.trivianight next`, with the only difference being that you need to - specify the question number. - - Parameters: - - question_number: An integer represents the question number to go to (i.e. .trivianight question 5). - """ - question = self.questions.next_question(question_number) - if isinstance(question, Embed): - await ctx.send(embed=question) - return - - (question_embed, time_limit), question_view = self.questions.current_question() + next_question.start() message = await ctx.send(embed=question_embed, view=question_view) - for time_remaining in range(time_limit, -1, -1): - if self.questions.view.active_question is False: - await ctx.send(embed=self.questions.end_question()) - await message.edit(embed=question_embed, view=None) - return - - await asyncio.sleep(1) - if time_remaining % 5 == 0 and time_remaining not in (time_limit, 0): - await ctx.send(f"{time_remaining}s remaining") + # Exponentially sleep less and less until the time limit is reached + percentage = 1 + while True: + percentage *= 0.5 + duration = next_question.time * percentage + + await asyncio.sleep(duration) + if int(duration) > 1: + # It is quite ugly to display decimals, the delay for requests to reach Discord + # cause sub-second accuracy to be quite pointless. + await ctx.send(f"{int(duration)}s remaining...") + else: + # Since each time we divide the percentage by 2 and sleep one half of the halves (then sleep a + # half, of that half) we must sleep both halves at the end. + await asyncio.sleep(duration) + break - await ctx.send(embed=self.questions.end_question()) + await ctx.send(embed=question_view.end_question()) await message.edit(embed=question_embed, view=None) + question_view.stop() @trivianight.command() @commands.has_any_role(*TRIVIA_NIGHT_ROLES) async def list(self, ctx: commands.Context) -> None: """ - Displays all the questions from the question bank. + Display all the questions left in the question bank. Questions are displayed in the following format: Q(number): Question description | :white_check_mark: if the question was used otherwise :x:. @@ -255,7 +201,15 @@ class TriviaNight(commands.Cog): The scoreboard view also has a button where the user can see their own rank, points and average speed if they didn't make it onto the leaderboard. """ - if self.questions.view.active_question is True: + if self.game is None: + await ctx.send(embed=Embed( + title=choice(NEGATIVE_REPLIES), + description="There is no trivia night running!", + color=Colours.soft_red + )) + return + + if self.game.current_question is not None: error_embed = Embed( title=choice(NEGATIVE_REPLIES), description="You can't end the event while a question is ongoing!", @@ -266,8 +220,9 @@ class TriviaNight(commands.Cog): scoreboard_embed, scoreboard_view = await self.scoreboard.display() await ctx.send(embed=scoreboard_embed, view=scoreboard_view) + self.game = None def setup(bot: Bot) -> None: """Load the TriviaNight cog.""" - bot.add_cog(TriviaNight(bot)) + bot.add_cog(TriviaNightCog(bot)) -- cgit v1.2.3 From 56d0786ead74a32e4fab3b74eef225292407d9cc Mon Sep 17 00:00:00 2001 From: Bluenix Date: Mon, 13 Dec 2021 17:59:48 +0100 Subject: Fix incorrect variable usage in list comprehension This appears to stem from a misunderstanding by me when restructuring the code. --- bot/exts/events/trivianight/_questions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 1d7bd4a9..9a2cb7d2 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -130,7 +130,7 @@ class QuestionView(View): if len(guesses) != 0: answers_chosen = { answer_choice: len( - tuple(filter(lambda x: x[0] == correct, guesses.values())) + tuple(filter(lambda x: x[0] == answer_choice, guesses.values())) ) / len(guesses) for answer_choice in labels } -- cgit v1.2.3 From 8c7baf05a82cf53813ed3eabc197abf7a0d98a63 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 19 Dec 2021 20:27:34 -0500 Subject: refactor --- bot/exts/events/trivianight/_game.py | 31 ++++++++++++++++++++++--- bot/exts/events/trivianight/_questions.py | 36 +++++++++++++++++++++++------- bot/exts/events/trivianight/_scoreboard.py | 36 +++++++++++++++++++----------- bot/exts/events/trivianight/trivianight.py | 27 +++++++++------------- 4 files changed, 90 insertions(+), 40 deletions(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index 7f2e48dc..db303c58 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -3,7 +3,6 @@ from random import randrange from string import ascii_uppercase from typing import Iterable, Optional, TypedDict - DEFAULT_QUESTION_POINTS = 10 DEFAULT_QUESTION_TIME = 10 @@ -56,7 +55,8 @@ class Question: @property def answers(self) -> list[tuple[str, str]]: - """The possible answers for this answer. + """ + The possible answers for this answer. This is a property that returns a list of letter, answer pairs. """ @@ -119,7 +119,11 @@ class TriviaNightGame: def __init__(self, data: list[QuestionData]) -> None: self._questions = [Question(q) for q in data] + # A copy of the questions to keep for `.trivianight list` + self._all_questions = list(self._questions) self.current_question: Optional[Question] = None + self._points = {} + self._speed = {} def __iter__(self) -> Iterable[Question]: return iter(self._questions) @@ -135,7 +139,7 @@ class TriviaNightGame: if number is not None: try: - question = [q for q in self._questions if q.number == number][0] + question = [q for q in self._all_questions if q.number == number][0] except IndexError: raise ValueError(f"Question number {number} does not exist.") else: @@ -156,3 +160,24 @@ class TriviaNightGame: self.current_question.stop() self.current_question = None + + def list_questions(self) -> None: + """ + List all the questions. + + This method should be called when `.trivianight list` is called to display the following information: + - Question number + - Question description + - Visited/not visited + """ + formatted_string = "" + + spaces = max(len(q.description) for q in self._all_questions) + + for question in self._all_questions: + visited, not_visited = ":checkmark:", ":x:" + formatted_string += f"`Q{question.number}: {question.description}" \ + f"{' ' * (spaces - len(question.description))}|`" \ + f" {visited if question not in self._all_questions else not_visited}\n" + + return formatted_string diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 9a2cb7d2..7fb6dedf 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -1,7 +1,5 @@ -from random import choice, randrange +from random import choice from string import ascii_uppercase -from time import perf_counter -from typing import Optional, TypedDict, Union import discord from discord import Embed, Interaction @@ -10,7 +8,7 @@ from discord.ui import Button, View from bot.constants import Colours, NEGATIVE_REPLIES from . import UserScore -from ._game import Question, AlreadyUpdated, QuestionClosed +from ._game import AlreadyUpdated, Question, QuestionClosed from ._scoreboard import Scoreboard @@ -110,7 +108,7 @@ class QuestionView(View): return question_embed - def end_question(self) -> Embed: + def end_question(self, scoreboard: Scoreboard) -> Embed: """ Ends the question and displays the statistics on who got the question correct, awards points, etc. @@ -120,7 +118,6 @@ class QuestionView(View): 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.question.number} was..", @@ -135,9 +132,13 @@ class QuestionView(View): for answer_choice in labels } - for idx, (answer, percent) in enumerate(answers_chosen.items()): + answers_chosen = dict( + sorted(list(answers_chosen.items()), key=lambda item: item[1], reverse=True) + ) + + for answer, percent in answers_chosen.items(): # Setting the color of answer_embed to the % of people that got it correct via the mapping - if idx == 0: + if dict(self.question.answers)[answer[0]] == self.question.correct: # Maps the % of people who got it right to a color, from a range of red to green percentage_to_color = [0xFC94A1, 0xFFCCCB, 0xCDFFCC, 0xB0F5AB, 0xB0F5AB] answer_embed.color = percentage_to_color[round(percent * 100) // 25] @@ -149,4 +150,23 @@ class QuestionView(View): inline=False ) + # Assign points to users + for user_id, answer in guesses.items(): + if dict(self.question.answers)[answer[0]] == self.question.correct: + scoreboard.assign_points( + UserScore(int(user_id)), + points=(1 - (answer[-1] / self.question.time) / 2) * self.question.max_points, + speed=answer[-1] + ) + elif answer[-1] <= 2: + scoreboard.assign_points( + UserScore(int(user_id)), + points=-(1 - (answer[-1] / self.question.time) / 2) * self.question.max_points + ) + else: + scoreboard.assign_points( + UserScore(int(user_id)), + points=0 + ) + return answer_embed diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 40f93475..babd1bd6 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -16,8 +16,6 @@ class ScoreboardView(View): def __init__(self, bot: Bot): super().__init__() self.bot = bot - self.points = {} - self.speed = {} async def create_main_leaderboard(self) -> Embed: """ @@ -142,19 +140,31 @@ class ScoreboardView(View): class Scoreboard: """Class for the scoreboard for the Trivia Night event.""" - def __setitem__(self, key: UserScore, value: dict): - if value.get("points") and key.user_id not in self.view.points.keys(): - self.view.points[key.user_id] = value["points"] - elif value.get("points"): - self.view.points[key.user_id] += self.view.points[key.user_id] - - if value.get("speed") and key.user_id not in self.view.speed.keys(): - self.view.speed[key.user_id] = [1, value["speed"]] - elif value.get("speed"): - self.view.speed[key.user_id] = [ - self.view.speed[key.user_id][0] + 1, self.view.speed[key.user_id][1] + value["speed"] + def __init__(self, bot: Bot): + self.view = ScoreboardView(bot) + self._points = {} + self._speed = {} + + def assign_points(self, user: UserScore, *, points: int = None, speed: float = None) -> None: + """ + Assign points or deduct points to/from a certain user. + + This method should be called once the question has finished and all answers have been registered. + """ + if points is not None and user.user_id not in self._points.keys(): + self._points[user.user_id] = points + elif points is not None: + self._points[user.user_id] += self._points[user.user_id] + + if speed is not None and user.user_id not in self._speed.keys(): + self._speed[user.user_id] = [1, speed] + elif speed is not None: + self._speed[user.user_id] = [ + self._speed[user.user_id][0] + 1, self._speed[user.user_id][1] + speed ] async def display(self) -> tuple[Embed, View]: """Returns the embed of the main leaderboard along with the ScoreboardView.""" + self.view.points = self._points + self.view.speed = self._speed return await self.view.create_main_leaderboard(), self.view diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 1465a03d..f158ec0c 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -4,7 +4,6 @@ from random import choice from typing import Optional from discord import Embed -from discord.colour import Color from discord.ext import commands from bot.bot import Bot @@ -12,7 +11,7 @@ from bot.constants import Colours, NEGATIVE_REPLIES, POSITIVE_REPLIES, Roles from ._game import TriviaNightGame from ._questions import QuestionView -from ._scoreboard import Scoreboard, ScoreboardView +from ._scoreboard import Scoreboard # The ID you see below is the Events Lead role ID TRIVIA_NIGHT_ROLES = (Roles.admin, 78361735739998228) @@ -24,6 +23,8 @@ class TriviaNightCog(commands.Cog): def __init__(self, bot: Bot): self.bot = bot self.game: Optional[TriviaNightGame] = None + self.scoreboard: Optional[Scoreboard] = None + self.question_closed: asyncio.Event = None @commands.group(aliases=["tn"], invoke_without_command=True) async def trivianight(self, ctx: commands.Context) -> None: @@ -91,12 +92,16 @@ class TriviaNightCog(commands.Cog): raise commands.BadArgument("Invalid JSON") self.game = TriviaNightGame(serialized_json) + self.question_closed = asyncio.Event() success_embed = Embed( title=choice(POSITIVE_REPLIES), description="The JSON was loaded successfully!", color=Colours.soft_green ) + + self.scoreboard = Scoreboard(self.bot) + await ctx.send(embed=success_embed) @trivianight.command(aliases=('next',)) @@ -140,6 +145,7 @@ class TriviaNightCog(commands.Cog): duration = next_question.time * percentage await asyncio.sleep(duration) + if int(duration) > 1: # It is quite ugly to display decimals, the delay for requests to reach Discord # cause sub-second accuracy to be quite pointless. @@ -150,7 +156,7 @@ class TriviaNightCog(commands.Cog): await asyncio.sleep(duration) break - await ctx.send(embed=question_view.end_question()) + await ctx.send(embed=question_view.end_question(self.scoreboard)) await message.edit(embed=question_embed, view=None) self.game.end_question() @@ -172,14 +178,7 @@ class TriviaNightCog(commands.Cog): )) return - # TODO: Because of how the game currently works, only the questions left will be able to - # be gotten. Iterate through self.game: - # - # for question in self.game: - # # This is an instance of Question from _game.py - # print(question.description) - - question_list = self.questions.list_questions() + question_list = self.game.list_questions() if isinstance(question_list, Embed): await ctx.send(embed=question_list) return @@ -211,10 +210,7 @@ class TriviaNightCog(commands.Cog): await ctx.send(embed=error_embed) return - # TODO: We need to tell the 'trivianight next' command that the game has ended, if it is still - # running that means it is currently counting down waiting to end the question. Use an asyncio.Event and - # asyncio.wait(self.lock.wait(), timeout=duration) as opposed to asyncio.sleep(duration). - self.game.end_question() + self.ongoing_question = False @trivianight.command() @commands.has_any_role(*TRIVIA_NIGHT_ROLES) @@ -245,7 +241,6 @@ class TriviaNightCog(commands.Cog): await ctx.send(embed=error_embed) return - # TODO: Refactor the scoreboard after the game simplification. scoreboard_embed, scoreboard_view = await self.scoreboard.display() await ctx.send(embed=scoreboard_embed, view=scoreboard_view) self.game = None -- cgit v1.2.3 From b6685b2acffc7958c0f960b3ab04ada731500d24 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 28 Dec 2021 00:28:33 -0500 Subject: remove UserScore --- bot/exts/events/trivianight/__init__.py | 7 ------- bot/exts/events/trivianight/_questions.py | 7 +++---- bot/exts/events/trivianight/_scoreboard.py | 18 ++++++++---------- 3 files changed, 11 insertions(+), 21 deletions(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/__init__.py b/bot/exts/events/trivianight/__init__.py index 87de18e0..e69de29b 100644 --- a/bot/exts/events/trivianight/__init__.py +++ b/bot/exts/events/trivianight/__init__.py @@ -1,7 +0,0 @@ -class UserScore: - """Marker class for passing into the scoreboard to add points/record speed.""" - - __slots__ = ("user_id",) - - def __init__(self, user_id: int): - self.user_id = user_id diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 7fb6dedf..0835d762 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -7,7 +7,6 @@ from discord.ui import Button, View from bot.constants import Colours, NEGATIVE_REPLIES -from . import UserScore from ._game import AlreadyUpdated, Question, QuestionClosed from ._scoreboard import Scoreboard @@ -154,18 +153,18 @@ class QuestionView(View): for user_id, answer in guesses.items(): if dict(self.question.answers)[answer[0]] == self.question.correct: scoreboard.assign_points( - UserScore(int(user_id)), + int(user_id), points=(1 - (answer[-1] / self.question.time) / 2) * self.question.max_points, speed=answer[-1] ) elif answer[-1] <= 2: scoreboard.assign_points( - UserScore(int(user_id)), + int(user_id), points=-(1 - (answer[-1] / self.question.time) / 2) * self.question.max_points ) else: scoreboard.assign_points( - UserScore(int(user_id)), + int(user_id), points=0 ) diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index babd1bd6..d9107dca 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -7,8 +7,6 @@ from discord.ui import Button, View from bot.bot import Bot from bot.constants import Colours, NEGATIVE_REPLIES -from . import UserScore - class ScoreboardView(View): """View for the scoreboard.""" @@ -145,22 +143,22 @@ class Scoreboard: self._points = {} self._speed = {} - def assign_points(self, user: UserScore, *, points: int = None, speed: float = None) -> None: + def assign_points(self, user_id: int, *, points: int = None, speed: float = None) -> None: """ Assign points or deduct points to/from a certain user. This method should be called once the question has finished and all answers have been registered. """ - if points is not None and user.user_id not in self._points.keys(): - self._points[user.user_id] = points + if points is not None and user_id not in self._points.keys(): + self._points[user_id] = points elif points is not None: - self._points[user.user_id] += self._points[user.user_id] + self._points[user_id] += self._points[user_id] - if speed is not None and user.user_id not in self._speed.keys(): - self._speed[user.user_id] = [1, speed] + if speed is not None and user_id not in self._speed.keys(): + self._speed[user_id] = [1, speed] elif speed is not None: - self._speed[user.user_id] = [ - self._speed[user.user_id][0] + 1, self._speed[user.user_id][1] + speed + self._speed[user_id] = [ + self._speed[user_id][0] + 1, self._speed[user_id][1] + speed ] async def display(self) -> tuple[Embed, View]: -- cgit v1.2.3 From d212af6ac1a965d64077559a593d092e03e5ba42 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 9 Jan 2022 17:25:22 -0500 Subject: sending the question is no longer accepting guesses as an ephemeral --- bot/exts/events/trivianight/_questions.py | 1 + 1 file changed, 1 insertion(+) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 0835d762..d729009d 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -46,6 +46,7 @@ class AnswerButton(Button): description="The question is no longer accepting guesses!", color=Colours.soft_red ), + ephemeral=True ) return -- cgit v1.2.3 From e7163dd5dd23ecc75d8b628262733510abf9d0d6 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 9 Jan 2022 18:10:59 -0500 Subject: use correct emoji name for .tn list --- bot/exts/events/trivianight/_game.py | 2 +- bot/exts/events/trivianight/_questions.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index 0b5fe562..9d8b98c1 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -175,7 +175,7 @@ class TriviaNightGame: spaces = max(len(q.description) for q in self._all_questions) for question in self._all_questions: - visited, not_visited = ":checkmark:", ":x:" + visited, not_visited = ":white_check_mark:", ":x:" formatted_string += f"`Q{question.number}: {question.description}" \ f"{' ' * (spaces - len(question.description))}|`" \ f" {visited if question not in self._questions else not_visited}\n" diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index d729009d..391e0a9e 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -128,7 +128,7 @@ class QuestionView(View): answers_chosen = { answer_choice: len( tuple(filter(lambda x: x[0] == answer_choice, guesses.values())) - ) / len(guesses) + ) for answer_choice in labels } @@ -136,16 +136,16 @@ class QuestionView(View): sorted(list(answers_chosen.items()), key=lambda item: item[1], reverse=True) ) - for answer, percent in answers_chosen.items(): + for answer, people_answered in answers_chosen.items(): # Setting the color of answer_embed to the % of people that got it correct via the mapping if dict(self.question.answers)[answer[0]] == self.question.correct: # Maps the % of people who got it right to a color, from a range of red to green percentage_to_color = [0xFC94A1, 0xFFCCCB, 0xCDFFCC, 0xB0F5AB, 0xB0F5AB] - answer_embed.color = percentage_to_color[round(percent * 100) // 25] + answer_embed.color = percentage_to_color[round(people_answered / len(guesses) * 100) // 25] # 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", + name=f"{people_answered / len(guesses) * 100:.1f}% of players chose", value=self.question.answers[ord(answer) - 65][1], inline=False ) -- cgit v1.2.3 From 60d131d44f9b3ccf06d1a2c0679e5daa3ae8b299 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 9 Jan 2022 18:18:17 -0500 Subject: QoL: adding an emoji and the number of people who answered for when the question is done --- bot/exts/events/trivianight/_questions.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 391e0a9e..013ffb0a 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -137,15 +137,22 @@ class QuestionView(View): ) for answer, people_answered in answers_chosen.items(): + is_correct_answer = dict(self.question.answers)[answer[0]] == self.question.correct + # Setting the color of answer_embed to the % of people that got it correct via the mapping - if dict(self.question.answers)[answer[0]] == self.question.correct: + if is_correct_answer: # Maps the % of people who got it right to a color, from a range of red to green percentage_to_color = [0xFC94A1, 0xFFCCCB, 0xCDFFCC, 0xB0F5AB, 0xB0F5AB] answer_embed.color = percentage_to_color[round(people_answered / len(guesses) * 100) // 25] + field_title = ( + (":white_check_mark: " if is_correct_answer else "") + + f"{people_answered} players (or {people_answered / len(guesses) * 100:.1f}% of players) chose" + ) + # The `ord` function is used here to change the letter to its corresponding position answer_embed.add_field( - name=f"{people_answered / len(guesses) * 100:.1f}% of players chose", + name=field_title, value=self.question.answers[ord(answer) - 65][1], inline=False ) -- cgit v1.2.3 From 570b138df7b2a30cff67bf99fa56173c557d45c3 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 9 Jan 2022 18:26:31 -0500 Subject: shorten field titles for after a question has been answered --- bot/exts/events/trivianight/_questions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 013ffb0a..2d337b40 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -147,7 +147,7 @@ class QuestionView(View): field_title = ( (":white_check_mark: " if is_correct_answer else "") - + f"{people_answered} players (or {people_answered / len(guesses) * 100:.1f}% of players) chose" + + f"{people_answered} players ({people_answered / len(guesses) * 100:.1f}%) chose" ) # The `ord` function is used here to change the letter to its corresponding position -- cgit v1.2.3 From 203923de98d5063d5fa8b1951a7fb01103a66957 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 10 Jan 2022 22:03:19 -0500 Subject: change .. to ... --- bot/exts/events/trivianight/_questions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'bot/exts/events/trivianight/_questions.py') diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 2d337b40..d6beced9 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -53,7 +53,7 @@ class AnswerButton(Button): if guess[1]: await interaction.response.send_message( embed=Embed( - title="Confirming that..", + title="Confirming that...", description=f"You chose answer {self.label}.", color=Colours.soft_green ), @@ -64,7 +64,7 @@ class AnswerButton(Button): # indicates that they changed it this time around. await interaction.response.send_message( embed=Embed( - title="Confirming that..", + title="Confirming that...", description=f"You changed your answer to answer {self.label}.", color=Colours.soft_green ), @@ -120,7 +120,7 @@ class QuestionView(View): labels = ascii_uppercase[:len(self.question.answers)] answer_embed = Embed( - title=f"The correct answer for Question {self.question.number} was..", + title=f"The correct answer for Question {self.question.number} was...", description=self.question.correct ) -- cgit v1.2.3