From 4ec414ad37c6c10218b80ebd1b028840c0f0f853 Mon Sep 17 00:00:00 2001 From: ToxicKidz Date: Tue, 29 Jun 2021 11:47:30 -0400 Subject: chore: Merge the .issue command into the github cog --- bot/exts/utilities/githubinfo.py | 273 +++++++++++++++++++++++++++++++++++++- bot/exts/utilities/issues.py | 277 --------------------------------------- 2 files changed, 267 insertions(+), 283 deletions(-) delete mode 100644 bot/exts/utilities/issues.py (limited to 'bot') diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index 539e388b..f0820731 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -1,5 +1,8 @@ import logging import random +import re +import typing as t +from dataclasses import dataclass from datetime import datetime from urllib.parse import quote, quote_plus @@ -7,24 +10,183 @@ import discord from discord.ext import commands from bot.bot import Bot -from bot.constants import Colours, NEGATIVE_REPLIES +from bot.constants import ( + Categories, + Channels, + Colours, + ERROR_REPLIES, + Emojis, + NEGATIVE_REPLIES, + Tokens, + WHITELISTED_CHANNELS +) from bot.exts.core.extensions import invoke_help_command +from bot.utils.decorators import whitelist_override log = logging.getLogger(__name__) GITHUB_API_URL = "https://api.github.com" +BAD_RESPONSE = { + 404: "Issue/pull request not located! Please enter a valid number!", + 403: "Rate limit has been hit! Please try again later!" +} +REQUEST_HEADERS = { + "Accept": "application/vnd.github.v3+json" +} + +REPOSITORY_ENDPOINT = "https://api.github.com/orgs/{org}/repos?per_page=100&type=public" +ISSUE_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/issues/{number}" +PR_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/pulls/{number}" + +if GITHUB_TOKEN := Tokens.github: + REQUEST_HEADERS["Authorization"] = f"token {GITHUB_TOKEN}" + +WHITELISTED_CATEGORIES = ( + Categories.development, Categories.devprojects, Categories.media, Categories.staff +) + +CODE_BLOCK_RE = re.compile( + r"^`([^`\n]+)`" # Inline codeblock + r"|```(.+?)```", # Multiline codeblock + re.DOTALL | re.MULTILINE +) + +# Maximum number of issues in one message +MAXIMUM_ISSUES = 5 + +# Regex used when looking for automatic linking in messages +# regex101 of current regex https://regex101.com/r/V2ji8M/6 +AUTOMATIC_REGEX = re.compile( + r"((?P[a-zA-Z0-9][a-zA-Z0-9\-]{1,39})\/)?(?P[\w\-\.]{1,100})#(?P[0-9]+)" +) + + +@dataclass +class FoundIssue: + """Dataclass representing an issue found by the regex.""" + + organisation: t.Optional[str] + repository: str + number: str + + def __hash__(self) -> int: + return hash((self.organisation, self.repository, self.number)) + + +@dataclass +class FetchError: + """Dataclass representing an error while fetching an issue.""" + + return_code: int + message: str + + +@dataclass +class IssueState: + """Dataclass representing the state of an issue.""" + + repository: str + number: int + url: str + title: str + emoji: str + class GithubInfo(commands.Cog): - """Fetches info from GitHub.""" + """A Cog that fetches info from GitHub.""" def __init__(self, bot: Bot): self.bot = bot + self.repos = [] + + @staticmethod + def remove_codeblocks(message: str) -> str: + """Remove any codeblock in a message.""" + return CODE_BLOCK_RE.sub("", message) + + async def fetch_issues( + self, + number: int, + repository: str, + user: str + ) -> t.Union[IssueState, FetchError]: + """ + Retrieve an issue from a GitHub repository. - async def fetch_data(self, url: str) -> dict: - """Retrieve data as a dictionary.""" - async with self.bot.http_session.get(url) as r: - return await r.json() + Returns IssueState on success, FetchError on failure. + """ + url = ISSUE_ENDPOINT.format(user=user, repository=repository, number=number) + pulls_url = PR_ENDPOINT.format(user=user, repository=repository, number=number) + log.trace(f"Querying GH issues API: {url}") + + async with self.bot.http_session.get(url, headers=REQUEST_HEADERS) as r: + json_data = await r.json() + + if r.status == 403: + if r.headers.get("X-RateLimit-Remaining") == "0": + log.info(f"Ratelimit reached while fetching {url}") + return FetchError(403, "Ratelimit reached, please retry in a few minutes.") + return FetchError(403, "Cannot access issue.") + elif r.status in (404, 410): + return FetchError(r.status, "Issue not found.") + elif r.status != 200: + return FetchError(r.status, "Error while fetching issue.") + + # The initial API request is made to the issues API endpoint, which will return information + # if the issue or PR is present. However, the scope of information returned for PRs differs + # from issues: if the 'issues' key is present in the response then we can pull the data we + # need from the initial API call. + if "issues" in json_data["html_url"]: + if json_data.get("state") == "open": + emoji = Emojis.issue_open + else: + emoji = Emojis.issue_closed + + # If the 'issues' key is not contained in the API response and there is no error code, then + # we know that a PR has been requested and a call to the pulls API endpoint is necessary + # to get the desired information for the PR. + else: + log.trace(f"PR provided, querying GH pulls API for additional information: {pulls_url}") + async with self.bot.http_session.get(pulls_url) as p: + pull_data = await p.json() + if pull_data["draft"]: + emoji = Emojis.pull_request_draft + elif pull_data["state"] == "open": + emoji = Emojis.pull_request_open + # When 'merged_at' is not None, this means that the state of the PR is merged + elif pull_data["merged_at"] is not None: + emoji = Emojis.pull_request_merged + else: + emoji = Emojis.pull_request_closed + + issue_url = json_data.get("html_url") + + return IssueState(repository, number, issue_url, json_data.get("title", ""), emoji) + + @staticmethod + def format_embed( + results: t.List[t.Union[IssueState, FetchError]], + user: str, + repository: t.Optional[str] = None + ) -> discord.Embed: + """Take a list of IssueState or FetchError and format a Discord embed for them.""" + description_list = [] + + for result in results: + if isinstance(result, IssueState): + description_list.append(f"{result.emoji} [{result.title}]({result.url})") + elif isinstance(result, FetchError): + description_list.append(f":x: [{result.return_code}] {result.message}") + + resp = discord.Embed( + colour=Colours.bright_green, + description="\n".join(description_list) + ) + + embed_url = f"https://github.com/{user}/{repository}" if repository else f"https://github.com/{user}" + resp.set_author(name="GitHub", url=embed_url) + return resp @commands.group(name="github", aliases=("gh", "git")) @commands.cooldown(1, 10, commands.BucketType.user) @@ -33,6 +195,105 @@ class GithubInfo(commands.Cog): if ctx.invoked_subcommand is None: await invoke_help_command(ctx) + @whitelist_override(channels=WHITELISTED_CHANNELS, categories=WHITELISTED_CATEGORIES) + @github_group.command(aliases=("pr", "issues", "prs"), root_aliases=("issue", "pr")) + async def issue( + self, + ctx: commands.Context, + numbers: commands.Greedy[int], + repository: str = "sir-lancebot", + user: str = "python-discord" + ) -> None: + """Command to retrieve issue(s) from a GitHub repository.""" + # Remove duplicates + numbers = set(numbers) + + err_message = None + if not numbers: + err_message = "You must have at least one issue/PR!" + + elif len(numbers) > MAXIMUM_ISSUES: + err_message = f"Too many issues/PRs! (maximum of {MAXIMUM_ISSUES})" + + # If there's an error with command invocation then send an error embed + if err_message is not None: + err_embed = discord.Embed( + title=random.choice(ERROR_REPLIES), + color=Colours.soft_red, + description=err_message + ) + await ctx.send(embed=err_embed) + await invoke_help_command(ctx) + return + + results = [await self.fetch_issues(number, repository, user) for number in numbers] + await ctx.send(embed=self.format_embed(results, user, repository)) + + @commands.Cog.listener() + async def on_message(self, message: discord.Message) -> None: + """ + Automatic issue linking. + + Listener to retrieve issue(s) from a GitHub repository using automatic linking if matching /#. + """ + # Ignore bots + if message.author.bot: + return + + issues = [ + FoundIssue(*match.group("org", "repo", "number")) + for match in AUTOMATIC_REGEX.finditer(self.remove_codeblocks(message.content)) + ] + links = [] + + if issues: + # Block this from working in DMs + if not message.guild: + await message.channel.send( + embed=discord.Embed( + title=random.choice(NEGATIVE_REPLIES), + description=( + "You can't retrieve issues from DMs. " + f"Try again in <#{Channels.community_bot_commands}>" + ), + colour=Colours.soft_red + ) + ) + return + + log.trace(f"Found {issues = }") + # Remove duplicates + issues = set(issues) + + if len(issues) > MAXIMUM_ISSUES: + embed = discord.Embed( + title=random.choice(ERROR_REPLIES), + color=Colours.soft_red, + description=f"Too many issues/PRs! (maximum of {MAXIMUM_ISSUES})" + ) + await message.channel.send(embed=embed, delete_after=5) + return + + for repo_issue in issues: + result = await self.fetch_issues( + int(repo_issue.number), + repo_issue.repository, + repo_issue.organisation or "python-discord" + ) + if isinstance(result, IssueState): + links.append(result) + + if not links: + return + + resp = self.format_embed(links, "python-discord") + await message.channel.send(embed=resp) + + async def fetch_data(self, url: str) -> dict: + """Retrieve data as a dictionary.""" + async with self.bot.http_session.get(url) as r: + return await r.json() + @github_group.command(name="user", aliases=("userinfo",)) async def github_user_info(self, ctx: commands.Context, username: str) -> None: """Fetches a user's GitHub information.""" diff --git a/bot/exts/utilities/issues.py b/bot/exts/utilities/issues.py deleted file mode 100644 index b6d5a43e..00000000 --- a/bot/exts/utilities/issues.py +++ /dev/null @@ -1,277 +0,0 @@ -import logging -import random -import re -from dataclasses import dataclass -from typing import Optional, Union - -import discord -from discord.ext import commands - -from bot.bot import Bot -from bot.constants import ( - Categories, Channels, Colours, ERROR_REPLIES, Emojis, NEGATIVE_REPLIES, Tokens, WHITELISTED_CHANNELS -) -from bot.utils.decorators import whitelist_override -from bot.utils.extensions import invoke_help_command - -log = logging.getLogger(__name__) - -BAD_RESPONSE = { - 404: "Issue/pull request not located! Please enter a valid number!", - 403: "Rate limit has been hit! Please try again later!" -} -REQUEST_HEADERS = { - "Accept": "application/vnd.github.v3+json" -} - -REPOSITORY_ENDPOINT = "https://api.github.com/orgs/{org}/repos?per_page=100&type=public" -ISSUE_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/issues/{number}" -PR_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/pulls/{number}" - -if GITHUB_TOKEN := Tokens.github: - REQUEST_HEADERS["Authorization"] = f"token {GITHUB_TOKEN}" - -WHITELISTED_CATEGORIES = ( - Categories.development, Categories.devprojects, Categories.media, Categories.staff -) - -CODE_BLOCK_RE = re.compile( - r"^`([^`\n]+)`" # Inline codeblock - r"|```(.+?)```", # Multiline codeblock - re.DOTALL | re.MULTILINE -) - -# Maximum number of issues in one message -MAXIMUM_ISSUES = 5 - -# Regex used when looking for automatic linking in messages -# regex101 of current regex https://regex101.com/r/V2ji8M/6 -AUTOMATIC_REGEX = re.compile( - r"((?P[a-zA-Z0-9][a-zA-Z0-9\-]{1,39})\/)?(?P[\w\-\.]{1,100})#(?P[0-9]+)" -) - - -@dataclass -class FoundIssue: - """Dataclass representing an issue found by the regex.""" - - organisation: Optional[str] - repository: str - number: str - - def __hash__(self) -> int: - return hash((self.organisation, self.repository, self.number)) - - -@dataclass -class FetchError: - """Dataclass representing an error while fetching an issue.""" - - return_code: int - message: str - - -@dataclass -class IssueState: - """Dataclass representing the state of an issue.""" - - repository: str - number: int - url: str - title: str - emoji: str - - -class Issues(commands.Cog): - """Cog that allows users to retrieve issues from GitHub.""" - - def __init__(self, bot: Bot): - self.bot = bot - self.repos = [] - - @staticmethod - def remove_codeblocks(message: str) -> str: - """Remove any codeblock in a message.""" - return re.sub(CODE_BLOCK_RE, "", message) - - async def fetch_issues( - self, - number: int, - repository: str, - user: str - ) -> Union[IssueState, FetchError]: - """ - Retrieve an issue from a GitHub repository. - - Returns IssueState on success, FetchError on failure. - """ - url = ISSUE_ENDPOINT.format(user=user, repository=repository, number=number) - pulls_url = PR_ENDPOINT.format(user=user, repository=repository, number=number) - log.trace(f"Querying GH issues API: {url}") - - async with self.bot.http_session.get(url, headers=REQUEST_HEADERS) as r: - json_data = await r.json() - - if r.status == 403: - if r.headers.get("X-RateLimit-Remaining") == "0": - log.info(f"Ratelimit reached while fetching {url}") - return FetchError(403, "Ratelimit reached, please retry in a few minutes.") - return FetchError(403, "Cannot access issue.") - elif r.status in (404, 410): - return FetchError(r.status, "Issue not found.") - elif r.status != 200: - return FetchError(r.status, "Error while fetching issue.") - - # The initial API request is made to the issues API endpoint, which will return information - # if the issue or PR is present. However, the scope of information returned for PRs differs - # from issues: if the 'issues' key is present in the response then we can pull the data we - # need from the initial API call. - if "issues" in json_data["html_url"]: - if json_data.get("state") == "open": - emoji = Emojis.issue_open - else: - emoji = Emojis.issue_closed - - # If the 'issues' key is not contained in the API response and there is no error code, then - # we know that a PR has been requested and a call to the pulls API endpoint is necessary - # to get the desired information for the PR. - else: - log.trace(f"PR provided, querying GH pulls API for additional information: {pulls_url}") - async with self.bot.http_session.get(pulls_url) as p: - pull_data = await p.json() - if pull_data["draft"]: - emoji = Emojis.pull_request_draft - elif pull_data["state"] == "open": - emoji = Emojis.pull_request_open - # When 'merged_at' is not None, this means that the state of the PR is merged - elif pull_data["merged_at"] is not None: - emoji = Emojis.pull_request_merged - else: - emoji = Emojis.pull_request_closed - - issue_url = json_data.get("html_url") - - return IssueState(repository, number, issue_url, json_data.get("title", ""), emoji) - - @staticmethod - def format_embed( - results: list[Union[IssueState, FetchError]], - user: str, - repository: Optional[str] = None - ) -> discord.Embed: - """Take a list of IssueState or FetchError and format a Discord embed for them.""" - description_list = [] - - for result in results: - if isinstance(result, IssueState): - description_list.append(f"{result.emoji} [{result.title}]({result.url})") - elif isinstance(result, FetchError): - description_list.append(f":x: [{result.return_code}] {result.message}") - - resp = discord.Embed( - colour=Colours.bright_green, - description="\n".join(description_list) - ) - - embed_url = f"https://github.com/{user}/{repository}" if repository else f"https://github.com/{user}" - resp.set_author(name="GitHub", url=embed_url) - return resp - - @whitelist_override(channels=WHITELISTED_CHANNELS, categories=WHITELISTED_CATEGORIES) - @commands.command(aliases=("issues", "pr", "prs")) - async def issue( - self, - ctx: commands.Context, - numbers: commands.Greedy[int], - repository: str = "sir-lancebot", - user: str = "python-discord" - ) -> None: - """Command to retrieve issue(s) from a GitHub repository.""" - # Remove duplicates - numbers = set(numbers) - - err_message = None - if not numbers: - err_message = "You must have at least one issue/PR!" - - elif len(numbers) > MAXIMUM_ISSUES: - err_message = f"Too many issues/PRs! (maximum of {MAXIMUM_ISSUES})" - - # If there's an error with command invocation then send an error embed - if err_message is not None: - err_embed = discord.Embed( - title=random.choice(ERROR_REPLIES), - color=Colours.soft_red, - description=err_message - ) - await ctx.send(embed=err_embed) - await invoke_help_command(ctx) - return - - results = [await self.fetch_issues(number, repository, user) for number in numbers] - await ctx.send(embed=self.format_embed(results, user, repository)) - - @commands.Cog.listener() - async def on_message(self, message: discord.Message) -> None: - """ - Automatic issue linking. - - Listener to retrieve issue(s) from a GitHub repository using automatic linking if matching /#. - """ - # Ignore bots - if message.author.bot: - return - - issues = [ - FoundIssue(*match.group("org", "repo", "number")) - for match in AUTOMATIC_REGEX.finditer(self.remove_codeblocks(message.content)) - ] - links = [] - - if issues: - # Block this from working in DMs - if not message.guild: - await message.channel.send( - embed=discord.Embed( - title=random.choice(NEGATIVE_REPLIES), - description=( - "You can't retrieve issues from DMs. " - f"Try again in <#{Channels.community_bot_commands}>" - ), - colour=Colours.soft_red - ) - ) - return - - log.trace(f"Found {issues = }") - # Remove duplicates - issues = set(issues) - - if len(issues) > MAXIMUM_ISSUES: - embed = discord.Embed( - title=random.choice(ERROR_REPLIES), - color=Colours.soft_red, - description=f"Too many issues/PRs! (maximum of {MAXIMUM_ISSUES})" - ) - await message.channel.send(embed=embed, delete_after=5) - return - - for repo_issue in issues: - result = await self.fetch_issues( - int(repo_issue.number), - repo_issue.repository, - repo_issue.organisation or "python-discord" - ) - if isinstance(result, IssueState): - links.append(result) - - if not links: - return - - resp = self.format_embed(links, "python-discord") - await message.channel.send(embed=resp) - - -def setup(bot: Bot) -> None: - """Load the Issues cog.""" - bot.add_cog(Issues(bot)) -- cgit v1.2.3 From ba10b9b6525beac6637e5a13ead03fb018751201 Mon Sep 17 00:00:00 2001 From: ToxicKidz Date: Sat, 31 Jul 2021 18:42:36 -0400 Subject: chore: Remove the .issue command --- bot/exts/utilities/githubinfo.py | 37 ------------------------------------- 1 file changed, 37 deletions(-) (limited to 'bot') diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index f0820731..b0b327b6 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -18,11 +18,8 @@ from bot.constants import ( Emojis, NEGATIVE_REPLIES, Tokens, - WHITELISTED_CHANNELS ) from bot.exts.core.extensions import invoke_help_command -from bot.utils.decorators import whitelist_override - log = logging.getLogger(__name__) GITHUB_API_URL = "https://api.github.com" @@ -195,40 +192,6 @@ class GithubInfo(commands.Cog): if ctx.invoked_subcommand is None: await invoke_help_command(ctx) - @whitelist_override(channels=WHITELISTED_CHANNELS, categories=WHITELISTED_CATEGORIES) - @github_group.command(aliases=("pr", "issues", "prs"), root_aliases=("issue", "pr")) - async def issue( - self, - ctx: commands.Context, - numbers: commands.Greedy[int], - repository: str = "sir-lancebot", - user: str = "python-discord" - ) -> None: - """Command to retrieve issue(s) from a GitHub repository.""" - # Remove duplicates - numbers = set(numbers) - - err_message = None - if not numbers: - err_message = "You must have at least one issue/PR!" - - elif len(numbers) > MAXIMUM_ISSUES: - err_message = f"Too many issues/PRs! (maximum of {MAXIMUM_ISSUES})" - - # If there's an error with command invocation then send an error embed - if err_message is not None: - err_embed = discord.Embed( - title=random.choice(ERROR_REPLIES), - color=Colours.soft_red, - description=err_message - ) - await ctx.send(embed=err_embed) - await invoke_help_command(ctx) - return - - results = [await self.fetch_issues(number, repository, user) for number in numbers] - await ctx.send(embed=self.format_embed(results, user, repository)) - @commands.Cog.listener() async def on_message(self, message: discord.Message) -> None: """ -- cgit v1.2.3 From 101001bfabae0cef37cf36ebbf4420f9d80736e4 Mon Sep 17 00:00:00 2001 From: ToxicKidz Date: Sat, 18 Sep 2021 10:28:35 -0400 Subject: chore: Apply suggested changes --- bot/exts/utilities/githubinfo.py | 90 ++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 55 deletions(-) (limited to 'bot') diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index b0b327b6..c9a65668 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -4,15 +4,15 @@ import re import typing as t from dataclasses import dataclass from datetime import datetime -from urllib.parse import quote, quote_plus +from urllib.parse import quote import discord +from aiohttp import ClientResponse from discord.ext import commands from bot.bot import Bot from bot.constants import ( Categories, - Channels, Colours, ERROR_REPLIES, Emojis, @@ -20,14 +20,11 @@ from bot.constants import ( Tokens, ) from bot.exts.core.extensions import invoke_help_command + log = logging.getLogger(__name__) GITHUB_API_URL = "https://api.github.com" -BAD_RESPONSE = { - 404: "Issue/pull request not located! Please enter a valid number!", - 403: "Rate limit has been hit! Please try again later!" -} REQUEST_HEADERS = { "Accept": "application/vnd.github.v3+json" } @@ -102,11 +99,11 @@ class GithubInfo(commands.Cog): """Remove any codeblock in a message.""" return CODE_BLOCK_RE.sub("", message) - async def fetch_issues( - self, - number: int, - repository: str, - user: str + async def fetch_issue( + self, + number: int, + repository: str, + user: str ) -> t.Union[IssueState, FetchError]: """ Retrieve an issue from a GitHub repository. @@ -117,8 +114,7 @@ class GithubInfo(commands.Cog): pulls_url = PR_ENDPOINT.format(user=user, repository=repository, number=number) log.trace(f"Querying GH issues API: {url}") - async with self.bot.http_session.get(url, headers=REQUEST_HEADERS) as r: - json_data = await r.json() + json_data, r = await self.fetch_data(url) if r.status == 403: if r.headers.get("X-RateLimit-Remaining") == "0": @@ -145,17 +141,17 @@ class GithubInfo(commands.Cog): # to get the desired information for the PR. else: log.trace(f"PR provided, querying GH pulls API for additional information: {pulls_url}") - async with self.bot.http_session.get(pulls_url) as p: - pull_data = await p.json() - if pull_data["draft"]: - emoji = Emojis.pull_request_draft - elif pull_data["state"] == "open": - emoji = Emojis.pull_request_open - # When 'merged_at' is not None, this means that the state of the PR is merged - elif pull_data["merged_at"] is not None: - emoji = Emojis.pull_request_merged - else: - emoji = Emojis.pull_request_closed + + pull_data, _ = await self.fetch_data(pulls_url) + if pull_data["draft"]: + emoji = Emojis.pull_request_draft + elif pull_data["state"] == "open": + emoji = Emojis.pull_request_open + # When 'merged_at' is not None, this means that the state of the PR is merged + elif pull_data["merged_at"] is not None: + emoji = Emojis.pull_request_merged + else: + emoji = Emojis.pull_request_closed issue_url = json_data.get("html_url") @@ -163,9 +159,7 @@ class GithubInfo(commands.Cog): @staticmethod def format_embed( - results: t.List[t.Union[IssueState, FetchError]], - user: str, - repository: t.Optional[str] = None + results: t.List[t.Union[IssueState, FetchError]] ) -> discord.Embed: """Take a list of IssueState or FetchError and format a Discord embed for them.""" description_list = [] @@ -181,8 +175,7 @@ class GithubInfo(commands.Cog): description="\n".join(description_list) ) - embed_url = f"https://github.com/{user}/{repository}" if repository else f"https://github.com/{user}" - resp.set_author(name="GitHub", url=embed_url) + resp.set_author(name="GitHub") return resp @commands.group(name="github", aliases=("gh", "git")) @@ -212,16 +205,6 @@ class GithubInfo(commands.Cog): if issues: # Block this from working in DMs if not message.guild: - await message.channel.send( - embed=discord.Embed( - title=random.choice(NEGATIVE_REPLIES), - description=( - "You can't retrieve issues from DMs. " - f"Try again in <#{Channels.community_bot_commands}>" - ), - colour=Colours.soft_red - ) - ) return log.trace(f"Found {issues = }") @@ -238,7 +221,7 @@ class GithubInfo(commands.Cog): return for repo_issue in issues: - result = await self.fetch_issues( + result = await self.fetch_issue( int(repo_issue.number), repo_issue.repository, repo_issue.organisation or "python-discord" @@ -249,19 +232,19 @@ class GithubInfo(commands.Cog): if not links: return - resp = self.format_embed(links, "python-discord") + resp = self.format_embed(links) await message.channel.send(embed=resp) - async def fetch_data(self, url: str) -> dict: - """Retrieve data as a dictionary.""" - async with self.bot.http_session.get(url) as r: - return await r.json() + async def fetch_data(self, url: str) -> tuple[dict[str], ClientResponse]: + """Retrieve data as a dictionary and the response in a tuple.""" + async with self.bot.http_session.get(url, heades=REQUEST_HEADERS) as r: + return await r.json(), r @github_group.command(name="user", aliases=("userinfo",)) async def github_user_info(self, ctx: commands.Context, username: str) -> None: """Fetches a user's GitHub information.""" async with ctx.typing(): - user_data = await self.fetch_data(f"{GITHUB_API_URL}/users/{quote_plus(username)}") + user_data, _ = await self.fetch_data(f"{GITHUB_API_URL}/users/{username}") # User_data will not have a message key if the user exists if "message" in user_data: @@ -274,7 +257,7 @@ class GithubInfo(commands.Cog): await ctx.send(embed=embed) return - org_data = await self.fetch_data(user_data["organizations_url"]) + org_data, _ = await self.fetch_data(user_data["organizations_url"]) orgs = [f"[{org['login']}](https://github.com/{org['login']})" for org in org_data] orgs_to_add = " | ".join(orgs) @@ -290,8 +273,8 @@ class GithubInfo(commands.Cog): embed = discord.Embed( title=f"`{user_data['login']}`'s GitHub profile info", - description=f"```\n{user_data['bio']}\n```\n" if user_data["bio"] else "", - colour=discord.Colour.og_blurple(), + description=f"```{user_data['bio']}```\n" if user_data["bio"] else "", + colour=discord.Colour.blurple(), url=user_data["html_url"], timestamp=datetime.strptime(user_data["created_at"], "%Y-%m-%dT%H:%M:%SZ") ) @@ -315,10 +298,7 @@ class GithubInfo(commands.Cog): ) if user_data["type"] == "User": - embed.add_field( - name="Gists", - value=f"[{gists}](https://gist.github.com/{quote_plus(username, safe='')})" - ) + embed.add_field(name="Gists", value=f"[{gists}](https://gist.github.com/{quote(username, safe='')})") embed.add_field( name=f"Organization{'s' if len(orgs)!=1 else ''}", @@ -347,7 +327,7 @@ class GithubInfo(commands.Cog): return async with ctx.typing(): - repo_data = await self.fetch_data(f"{GITHUB_API_URL}/repos/{quote(repo)}") + repo_data, _ = await self.fetch_data(f"{GITHUB_API_URL}/repos/{quote(repo)}") # There won't be a message key if this repo exists if "message" in repo_data: @@ -363,7 +343,7 @@ class GithubInfo(commands.Cog): embed = discord.Embed( title=repo_data["name"], description=repo_data["description"], - colour=discord.Colour.og_blurple(), + colour=discord.Colour.blurple(), url=repo_data["html_url"] ) -- cgit v1.2.3 From cb4114823b91056d9b552d3e75c3c8ca9e879da7 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Thu, 2 Dec 2021 11:33:57 +0000 Subject: Make dataclasses hashable, and fix kwarg spelling error --- bot/exts/utilities/githubinfo.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) (limited to 'bot') diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index c9a65668..ee05497a 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -11,14 +11,7 @@ from aiohttp import ClientResponse from discord.ext import commands from bot.bot import Bot -from bot.constants import ( - Categories, - Colours, - ERROR_REPLIES, - Emojis, - NEGATIVE_REPLIES, - Tokens, -) +from bot.constants import Categories, Colours, ERROR_REPLIES, Emojis, NEGATIVE_REPLIES, Tokens from bot.exts.core.extensions import invoke_help_command log = logging.getLogger(__name__) @@ -41,7 +34,7 @@ WHITELISTED_CATEGORIES = ( ) CODE_BLOCK_RE = re.compile( - r"^`([^`\n]+)`" # Inline codeblock + r"^`([^`\n]+)`" # Inline codeblock r"|```(.+?)```", # Multiline codeblock re.DOTALL | re.MULTILINE ) @@ -56,7 +49,7 @@ AUTOMATIC_REGEX = re.compile( ) -@dataclass +@dataclass(eq=True, frozen=True) class FoundIssue: """Dataclass representing an issue found by the regex.""" @@ -64,11 +57,8 @@ class FoundIssue: repository: str number: str - def __hash__(self) -> int: - return hash((self.organisation, self.repository, self.number)) - -@dataclass +@dataclass(eq=True, frozen=True) class FetchError: """Dataclass representing an error while fetching an issue.""" @@ -76,7 +66,7 @@ class FetchError: message: str -@dataclass +@dataclass(eq=True, frozen=True) class IssueState: """Dataclass representing the state of an issue.""" @@ -237,7 +227,7 @@ class GithubInfo(commands.Cog): async def fetch_data(self, url: str) -> tuple[dict[str], ClientResponse]: """Retrieve data as a dictionary and the response in a tuple.""" - async with self.bot.http_session.get(url, heades=REQUEST_HEADERS) as r: + async with self.bot.http_session.get(url, headers=REQUEST_HEADERS) as r: return await r.json(), r @github_group.command(name="user", aliases=("userinfo",)) -- cgit v1.2.3 From cdd4067ad7497b48440074494aa4de15a908f7d5 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Thu, 2 Dec 2021 11:42:04 +0000 Subject: use og_blurple in issue embed for consistency --- bot/exts/utilities/githubinfo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'bot') diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index ee05497a..b7dbe64d 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -264,7 +264,7 @@ class GithubInfo(commands.Cog): embed = discord.Embed( title=f"`{user_data['login']}`'s GitHub profile info", description=f"```{user_data['bio']}```\n" if user_data["bio"] else "", - colour=discord.Colour.blurple(), + colour=discord.Colour.og_blurple(), url=user_data["html_url"], timestamp=datetime.strptime(user_data["created_at"], "%Y-%m-%dT%H:%M:%SZ") ) @@ -333,7 +333,7 @@ class GithubInfo(commands.Cog): embed = discord.Embed( title=repo_data["name"], description=repo_data["description"], - colour=discord.Colour.blurple(), + colour=discord.Colour.og_blurple(), url=repo_data["html_url"] ) -- cgit v1.2.3 From e584697e0809e60cda899b2148a1efcff993177a Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 3 Dec 2021 18:38:42 +0000 Subject: Move logging and remove unused varibales in GitHubInfo cog --- bot/exts/utilities/githubinfo.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) (limited to 'bot') diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index b7dbe64d..009e0fad 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -11,7 +11,7 @@ from aiohttp import ClientResponse from discord.ext import commands from bot.bot import Bot -from bot.constants import Categories, Colours, ERROR_REPLIES, Emojis, NEGATIVE_REPLIES, Tokens +from bot.constants import Colours, ERROR_REPLIES, Emojis, NEGATIVE_REPLIES, Tokens from bot.exts.core.extensions import invoke_help_command log = logging.getLogger(__name__) @@ -26,12 +26,8 @@ REPOSITORY_ENDPOINT = "https://api.github.com/orgs/{org}/repos?per_page=100&type ISSUE_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/issues/{number}" PR_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/pulls/{number}" -if GITHUB_TOKEN := Tokens.github: - REQUEST_HEADERS["Authorization"] = f"token {GITHUB_TOKEN}" - -WHITELISTED_CATEGORIES = ( - Categories.development, Categories.devprojects, Categories.media, Categories.staff -) +if Tokens.github: + REQUEST_HEADERS["Authorization"] = f"token {Tokens.github}" CODE_BLOCK_RE = re.compile( r"^`([^`\n]+)`" # Inline codeblock @@ -102,7 +98,6 @@ class GithubInfo(commands.Cog): """ url = ISSUE_ENDPOINT.format(user=user, repository=repository, number=number) pulls_url = PR_ENDPOINT.format(user=user, repository=repository, number=number) - log.trace(f"Querying GH issues API: {url}") json_data, r = await self.fetch_data(url) @@ -130,8 +125,6 @@ class GithubInfo(commands.Cog): # we know that a PR has been requested and a call to the pulls API endpoint is necessary # to get the desired information for the PR. else: - log.trace(f"PR provided, querying GH pulls API for additional information: {pulls_url}") - pull_data, _ = await self.fetch_data(pulls_url) if pull_data["draft"]: emoji = Emojis.pull_request_draft @@ -227,6 +220,7 @@ class GithubInfo(commands.Cog): async def fetch_data(self, url: str) -> tuple[dict[str], ClientResponse]: """Retrieve data as a dictionary and the response in a tuple.""" + log.trace(f"Querying GH issues API: {url}") async with self.bot.http_session.get(url, headers=REQUEST_HEADERS) as r: return await r.json(), r -- cgit v1.2.3 From caf36c69a06e2c87d258b61afc17d80d53835c79 Mon Sep 17 00:00:00 2001 From: Xithrius <15021300+Xithrius@users.noreply.github.com> Date: Thu, 20 Jan 2022 17:59:10 -0800 Subject: Added even more conversation starter topics. (#1014) --- bot/resources/utilities/py_topics.yaml | 26 +++++++++++++++++++++++--- bot/resources/utilities/starter.yaml | 3 +-- 2 files changed, 24 insertions(+), 5 deletions(-) (limited to 'bot') diff --git a/bot/resources/utilities/py_topics.yaml b/bot/resources/utilities/py_topics.yaml index 1cd2c325..4f684841 100644 --- a/bot/resources/utilities/py_topics.yaml +++ b/bot/resources/utilities/py_topics.yaml @@ -35,12 +35,22 @@ - Have you ever worked with a microcontroller or anything physical with Python before? - Have you ever tried making your own programming language? - Has a recently discovered Python module changed your general use of Python? + - What is your motivation for programming? + - What's your favorite Python related book? + - What's your favorite use of recursion in Python? + - If you could change one thing in Python, what would it be? + - What third-party library do you wish was in the Python standard library? + - Which package do you use the most and why? + - Which Python feature do you love the most? + - Do you have any plans for future projects? + - What modules/libraries do you want to see more projects using? + - What's the most ambitious thing you've done with Python so far? # algos-and-data-structs 650401909852864553: - -# async +# async-and-concurrency 630504881542791169: - Are there any frameworks you wish were async? - How have coroutines changed the way you write Python? @@ -54,12 +64,13 @@ 342318764227821568: - Where do you get your best data? - What is your preferred database and for what use? + - What is the least safe use of databases you've seen? -# data-science +# data-science-and-ai 366673247892275221: - -# discord.py +# discord-bots 343944376055103488: - What unique features does your bot contain, if any? - What commands/features are you proud of making? @@ -78,6 +89,8 @@ - What's a common part of programming we can make harder? - What are the pros and cons of messing with __magic__()? - What's your favorite Python hack? + - What's the weirdest language feature that Python doesn't have, and how can we change that? + - What is the most esoteric code you've written? # game-development 660625198390837248: @@ -110,6 +123,10 @@ - How often do you use GitHub Actions and workflows to automate your repositories? - What's your favorite app on GitHub? +# type-hinting +891788761371906108: + - + # unit-testing 463035728335732738: - @@ -120,6 +137,7 @@ - What's your most used Bash command? - How often do you update your Unix machine? - How often do you upgrade on production? + - What is your least favorite thing about interoperability amongst *NIX operating systems and/or platforms? # user-interfaces 338993628049571840: @@ -128,6 +146,7 @@ - Do you perfer Command Line Interfaces (CLI) or Graphic User Interfaces (GUI)? - What's your favorite CLI (Command Line Interface) or TUI (Terminal Line Interface)? - What's your best GUI project? + - What the best-looking app you've used? # web-development 366673702533988363: @@ -136,3 +155,4 @@ - What is your favorite API library? - What do you use for your frontend? - What does your stack look like? + - What's the best-looking website you've visited? diff --git a/bot/resources/utilities/starter.yaml b/bot/resources/utilities/starter.yaml index 6b0de0ef..ce759e1a 100644 --- a/bot/resources/utilities/starter.yaml +++ b/bot/resources/utilities/starter.yaml @@ -32,8 +32,6 @@ - How many years have you spent coding? - What book do you highly recommend everyone to read? - What websites do you use daily to keep yourself up to date with the industry? -- What made you want to join this Discord server? -- How are you? - What is the best advice you have ever gotten in regards to programming/software? - What is the most satisfying thing you've done in your life? - Who is your favorite music composer/producer/singer? @@ -49,3 +47,4 @@ - What artistic talents do you have? - What is the tallest building you've entered? - What is the oldest computer you've ever used? +- What animals do you like? -- cgit v1.2.3 From 1fd39adbb53b14015033fcaff0cbfb4aa81c65d7 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Tue, 25 Jan 2022 12:13:07 +0300 Subject: Clean Up Constants File This commit moves the logging constants from the Client class to a new Logging class. Flake8 is renabled for constants, and the error it was disabled for was specifically ignored. All new errors were fixed. It also fixes a bug with __all__, which was trying to export a missing symbol due to a missing comma. Signed-off-by: Hassan Abouelela --- bot/constants.py | 13 ++++++++++--- bot/log.py | 8 ++++---- tox.ini | 4 +++- 3 files changed, 17 insertions(+), 8 deletions(-) (limited to 'bot') diff --git a/bot/constants.py b/bot/constants.py index 3b426c47..7e7ee749 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -12,6 +12,7 @@ __all__ = ( "Channels", "Categories", "Client", + "Logging", "Colours", "Emojis", "Icons", @@ -23,7 +24,7 @@ __all__ = ( "Reddit", "RedisConfig", "RedirectOutput", - "PYTHON_PREFIX" + "PYTHON_PREFIX", "MODERATION_ROLES", "STAFF_ROLES", "WHITELISTED_CHANNELS", @@ -37,6 +38,7 @@ log = logging.getLogger(__name__) PYTHON_PREFIX = "!" + @dataclasses.dataclass class AdventOfCodeLeaderboard: id: str @@ -130,18 +132,24 @@ class Categories(NamedTuple): media = 799054581991997460 staff = 364918151625965579 + codejam_categories_name = "Code Jam" # Name of the codejam team categories + class Client(NamedTuple): name = "Sir Lancebot" guild = int(environ.get("BOT_GUILD", 267624335836053506)) prefix = environ.get("PREFIX", ".") token = environ.get("BOT_TOKEN") debug = environ.get("BOT_DEBUG", "true").lower() == "true" - file_logs = environ.get("FILE_LOGS", "false").lower() == "true" github_bot_repo = "https://github.com/python-discord/sir-lancebot" # Override seasonal locks: 1 (January) to 12 (December) month_override = int(environ["MONTH_OVERRIDE"]) if "MONTH_OVERRIDE" in environ else None + + +class Logging(NamedTuple): + debug = Client.debug + file_logs = environ.get("FILE_LOGS", "false").lower() == "true" trace_loggers = environ.get("BOT_TRACE_LOGGERS") @@ -231,7 +239,6 @@ class Emojis: status_dnd = "<:status_dnd:470326272082313216>" status_offline = "<:status_offline:470326266537705472>" - stackoverflow_tag = "<:stack_tag:870926975307501570>" stackoverflow_views = "<:stack_eye:870926992692879371>" diff --git a/bot/log.py b/bot/log.py index 29e696e0..a87a836a 100644 --- a/bot/log.py +++ b/bot/log.py @@ -6,7 +6,7 @@ from pathlib import Path import coloredlogs -from bot.constants import Client +from bot.constants import Logging def setup() -> None: @@ -20,7 +20,7 @@ def setup() -> None: log_format = logging.Formatter(format_string) root_logger = logging.getLogger() - if Client.file_logs: + if Logging.file_logs: # Set up file logging log_file = Path("logs/sir-lancebot.log") log_file.parent.mkdir(exist_ok=True) @@ -45,7 +45,7 @@ def setup() -> None: coloredlogs.install(level=logging.TRACE, stream=sys.stdout) - root_logger.setLevel(logging.DEBUG if Client.debug else logging.INFO) + root_logger.setLevel(logging.DEBUG if Logging.debug else logging.INFO) # Silence irrelevant loggers logging.getLogger("discord").setLevel(logging.ERROR) logging.getLogger("websockets").setLevel(logging.ERROR) @@ -81,7 +81,7 @@ def _set_trace_loggers() -> None: Otherwise if the env var begins with a "*", the root logger is set to the trace level and other contents are ignored. """ - level_filter = Client.trace_loggers + level_filter = Logging.trace_loggers if level_filter: if level_filter.startswith("*"): logging.getLogger().setLevel(logging.TRACE) diff --git a/tox.ini b/tox.ini index f561fcd9..61ff9616 100644 --- a/tox.ini +++ b/tox.ini @@ -20,5 +20,7 @@ exclude= __pycache__,.cache, venv,.venv, tests, - constants.py +per-file-ignores = + # Don't require docstrings in constants + constants.py:D101 import-order-style=pycharm -- cgit v1.2.3 From 335620341046e3b7be547ac9f18d25d1fb9bec55 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Tue, 25 Jan 2022 12:15:50 +0300 Subject: Reduce AOC Logging Output The AOC cog produces a lot of large logs very frequently which have minimal value, causing the logs to be significantly harder to navigate. Signed-off-by: Hassan Abouelela --- bot/constants.py | 2 +- bot/exts/events/advent_of_code/_helpers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'bot') diff --git a/bot/constants.py b/bot/constants.py index 7e7ee749..d39f7361 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -55,7 +55,7 @@ class AdventOfCodeLeaderboard: def session(self) -> str: """Return either the actual `session` cookie or the fallback cookie.""" if self.use_fallback_session: - log.info(f"Returning fallback cookie for board `{self.id}`.") + log.trace(f"Returning fallback cookie for board `{self.id}`.") return AdventOfCode.fallback_session return self._session diff --git a/bot/exts/events/advent_of_code/_helpers.py b/bot/exts/events/advent_of_code/_helpers.py index 15b1329d..6c004901 100644 --- a/bot/exts/events/advent_of_code/_helpers.py +++ b/bot/exts/events/advent_of_code/_helpers.py @@ -255,7 +255,7 @@ async def _fetch_leaderboard_data() -> dict[str, Any]: # Two attempts, one with the original session cookie and one with the fallback session for attempt in range(1, 3): - log.info(f"Attempting to fetch leaderboard `{leaderboard.id}` ({attempt}/2)") + log.debug(f"Attempting to fetch leaderboard `{leaderboard.id}` ({attempt}/2)") cookies = {"session": leaderboard.session} try: raw_data = await _leaderboard_request(leaderboard_url, leaderboard.id, cookies) -- cgit v1.2.3 From 7fc90a6cb9e1e867d0fdfe52ae3d0a50c4f07c05 Mon Sep 17 00:00:00 2001 From: Ben Soyka Date: Mon, 31 Jan 2022 17:38:35 -0700 Subject: Fix footer reference to deprecated unsubscribe command --- bot/exts/holidays/valentines/lovecalculator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/holidays/valentines/lovecalculator.py b/bot/exts/holidays/valentines/lovecalculator.py index a53014e5..277c9a39 100644 --- a/bot/exts/holidays/valentines/lovecalculator.py +++ b/bot/exts/holidays/valentines/lovecalculator.py @@ -90,7 +90,7 @@ class LoveCalculator(Cog): name="A letter from Dr. Love:", value=data["text"] ) - embed.set_footer(text=f"You can unsubscribe from lovefest by using {Client.prefix}lovefest unsub") + embed.set_footer(text="You can unsubscribe from lovefest by using !subscribe") await ctx.send(embed=embed) -- cgit v1.2.3 From 851e6adb03e54244ef59c75a41783d9ec548ab69 Mon Sep 17 00:00:00 2001 From: Ben Soyka Date: Mon, 31 Jan 2022 18:55:45 -0700 Subject: Add period to love calculator footer --- bot/exts/holidays/valentines/lovecalculator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/holidays/valentines/lovecalculator.py b/bot/exts/holidays/valentines/lovecalculator.py index 277c9a39..d49d59d4 100644 --- a/bot/exts/holidays/valentines/lovecalculator.py +++ b/bot/exts/holidays/valentines/lovecalculator.py @@ -90,7 +90,7 @@ class LoveCalculator(Cog): name="A letter from Dr. Love:", value=data["text"] ) - embed.set_footer(text="You can unsubscribe from lovefest by using !subscribe") + embed.set_footer(text="You can unsubscribe from lovefest by using !subscribe.") await ctx.send(embed=embed) -- cgit v1.2.3 From 3b0217eb43c8cf01dc0d387ff64639d3f1cfe2fe Mon Sep 17 00:00:00 2001 From: Ben Soyka Date: Mon, 31 Jan 2022 21:02:03 -0700 Subject: Use constant for Python bot prefix --- bot/exts/holidays/valentines/lovecalculator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'bot') diff --git a/bot/exts/holidays/valentines/lovecalculator.py b/bot/exts/holidays/valentines/lovecalculator.py index d49d59d4..c2860fb2 100644 --- a/bot/exts/holidays/valentines/lovecalculator.py +++ b/bot/exts/holidays/valentines/lovecalculator.py @@ -12,7 +12,7 @@ from discord.ext import commands from discord.ext.commands import BadArgument, Cog, clean_content from bot.bot import Bot -from bot.constants import Channels, Client, Lovefest, Month +from bot.constants import Channels, Client, Lovefest, Month, PYTHON_PREFIX from bot.utils.decorators import in_month log = logging.getLogger(__name__) @@ -90,7 +90,7 @@ class LoveCalculator(Cog): name="A letter from Dr. Love:", value=data["text"] ) - embed.set_footer(text="You can unsubscribe from lovefest by using !subscribe.") + embed.set_footer(text=f"You can unsubscribe from lovefest by using {PYTHON_PREFIX}subscribe.") await ctx.send(embed=embed) -- cgit v1.2.3 From a400f81de9f14aba698c47f965824cb7e9b4ed10 Mon Sep 17 00:00:00 2001 From: Ben Soyka Date: Mon, 31 Jan 2022 21:04:46 -0700 Subject: Replace command/channel for lovefest role error in calculator --- bot/exts/holidays/valentines/lovecalculator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/holidays/valentines/lovecalculator.py b/bot/exts/holidays/valentines/lovecalculator.py index c2860fb2..367eacaf 100644 --- a/bot/exts/holidays/valentines/lovecalculator.py +++ b/bot/exts/holidays/valentines/lovecalculator.py @@ -51,7 +51,7 @@ class LoveCalculator(Cog): raise BadArgument( "This command can only be ran against members with the lovefest role! " "This role be can assigned by running " - f"`{Client.prefix}lovefest sub` in <#{Channels.community_bot_commands}>." + f"`{PYTHON_PREFIX}subscribe` in <#{Channels.bot}>." ) if whom is None: -- cgit v1.2.3 From 74885a48306b30945b9899a4d44a64ba84b4c24c Mon Sep 17 00:00:00 2001 From: Ben Soyka Date: Mon, 31 Jan 2022 21:08:05 -0700 Subject: Remove unused import --- bot/exts/holidays/valentines/lovecalculator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/holidays/valentines/lovecalculator.py b/bot/exts/holidays/valentines/lovecalculator.py index 367eacaf..99fba150 100644 --- a/bot/exts/holidays/valentines/lovecalculator.py +++ b/bot/exts/holidays/valentines/lovecalculator.py @@ -12,7 +12,7 @@ from discord.ext import commands from discord.ext.commands import BadArgument, Cog, clean_content from bot.bot import Bot -from bot.constants import Channels, Client, Lovefest, Month, PYTHON_PREFIX +from bot.constants import Channels, Lovefest, Month, PYTHON_PREFIX from bot.utils.decorators import in_month log = logging.getLogger(__name__) -- cgit v1.2.3 From 2447c51a4dc74d48872caa58fe8f5cc350cf3754 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Wed, 13 Oct 2021 20:22:54 -0400 Subject: initial commit --- bot/exts/utilities/trivianight.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 bot/exts/utilities/trivianight.py (limited to 'bot') diff --git a/bot/exts/utilities/trivianight.py b/bot/exts/utilities/trivianight.py new file mode 100644 index 00000000..29a9e3d1 --- /dev/null +++ b/bot/exts/utilities/trivianight.py @@ -0,0 +1,15 @@ +from discord.ext import commands + +from bot.bot import Bot + + +class TriviaNight(commands.Cog): + """Cog for the Python Trivia Night event.""" + + def __init__(self, bot: Bot): + self.bot = bot + + +def setup(bot: Bot) -> None: + """Load the TriviaNight cog.""" + bot.add_cog(TriviaNight(bot)) -- cgit v1.2.3 From a4b2facdd51ed7cff7e045e62d12c93797df0e11 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Wed, 13 Oct 2021 22:02:37 -0400 Subject: organizing score board (scoreboard.py) --- bot/exts/events/trivianight/__init__.py | 0 bot/exts/events/trivianight/scoreboard.py | 59 ++++++++++++++++++++++++++++++ bot/exts/events/trivianight/trivianight.py | 15 ++++++++ bot/exts/utilities/trivianight.py | 15 -------- 4 files changed, 74 insertions(+), 15 deletions(-) create mode 100644 bot/exts/events/trivianight/__init__.py create mode 100644 bot/exts/events/trivianight/scoreboard.py create mode 100644 bot/exts/events/trivianight/trivianight.py delete mode 100644 bot/exts/utilities/trivianight.py (limited to 'bot') diff --git a/bot/exts/events/trivianight/__init__.py b/bot/exts/events/trivianight/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bot/exts/events/trivianight/scoreboard.py b/bot/exts/events/trivianight/scoreboard.py new file mode 100644 index 00000000..4e94e361 --- /dev/null +++ b/bot/exts/events/trivianight/scoreboard.py @@ -0,0 +1,59 @@ +import discord.ui +from discord import ButtonStyle, Embed, Interaction +from discord.ui import Button, View + +from bot.constants import Colours + + +class ScoreboardView(View): + """View for the scoreboard.""" + + def __init__(self): + self.points = {} + self.speed = {} + + def create_speed_embed(self) -> None: + """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]: + speed_embed.add_field( + name=user, value=f"`{(time_taken[1] / time_taken[0]):.3f}s` (on average)", inline=False + ) + + return speed_embed + + @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) + + +class Scoreboard: + """Class for the scoreboard for the trivianight event.""" + + def __init__(self, view: View): + self.view = view + + 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.removepreix("speed: ")] diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py new file mode 100644 index 00000000..29a9e3d1 --- /dev/null +++ b/bot/exts/events/trivianight/trivianight.py @@ -0,0 +1,15 @@ +from discord.ext import commands + +from bot.bot import Bot + + +class TriviaNight(commands.Cog): + """Cog for the Python Trivia Night event.""" + + def __init__(self, bot: Bot): + self.bot = bot + + +def setup(bot: Bot) -> None: + """Load the TriviaNight cog.""" + bot.add_cog(TriviaNight(bot)) diff --git a/bot/exts/utilities/trivianight.py b/bot/exts/utilities/trivianight.py deleted file mode 100644 index 29a9e3d1..00000000 --- a/bot/exts/utilities/trivianight.py +++ /dev/null @@ -1,15 +0,0 @@ -from discord.ext import commands - -from bot.bot import Bot - - -class TriviaNight(commands.Cog): - """Cog for the Python Trivia Night event.""" - - def __init__(self, bot: Bot): - self.bot = bot - - -def setup(bot: Bot) -> None: - """Load the TriviaNight cog.""" - bot.add_cog(TriviaNight(bot)) -- cgit v1.2.3 From 9d347be892925f7bc431ae7177e54fee0503996d Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 17 Oct 2021 23:10:15 -0400 Subject: question view shell finished --- bot/exts/events/trivianight/questions.py | 71 +++++++++++++++++++++++++++++++ bot/exts/events/trivianight/scoreboard.py | 34 ++++++++++++--- 2 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 bot/exts/events/trivianight/questions.py (limited to 'bot') diff --git a/bot/exts/events/trivianight/questions.py b/bot/exts/events/trivianight/questions.py new file mode 100644 index 00000000..21277ce9 --- /dev/null +++ b/bot/exts/events/trivianight/questions.py @@ -0,0 +1,71 @@ +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, scoreboard: Scoreboard): + self.scoreboard = scoreboard + self.current_question = {} + self._users_picked = {} + + 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")] + return question_embed + + def end_question(self) -> dict: + """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)) + + return return_dict diff --git a/bot/exts/events/trivianight/scoreboard.py b/bot/exts/events/trivianight/scoreboard.py index 4e94e361..34535690 100644 --- a/bot/exts/events/trivianight/scoreboard.py +++ b/bot/exts/events/trivianight/scoreboard.py @@ -1,18 +1,34 @@ +from typing import Union + import discord.ui from discord import ButtonStyle, Embed, Interaction from discord.ui import Button, View +from bot.bot import Bot from bot.constants import Colours class ScoreboardView(View): """View for the scoreboard.""" - def __init__(self): + def __init__(self, bot: Bot): + self.bot = bot self.points = {} self.speed = {} - def create_speed_embed(self) -> None: + 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) + + return main_embed + + 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", @@ -21,7 +37,9 @@ class ScoreboardView(View): ) for user, time_taken in list(self.speed.items())[:10]: speed_embed.add_field( - name=user, value=f"`{(time_taken[1] / time_taken[0]):.3f}s` (on average)", inline=False + name=self.bot.get_user(user), + value=f"`{(time_taken[1] / time_taken[0]):.3f}s` (on average)", + inline=False ) return speed_embed @@ -29,14 +47,14 @@ 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=self._create_speed_embed(), ephemeral=True) class Scoreboard: """Class for the scoreboard for the trivianight event.""" - def __init__(self, view: View): - self.view = view + def __init__(self): + self.view = ScoreboardView() def __setitem__(self, key: str, value: int): if key.startswith("points: "): @@ -57,3 +75,7 @@ class Scoreboard: return self.view.points[item.removeprefix("points: ")] elif item.startswith("speed: "): return self.view.speed[item.removepreix("speed: ")] + + 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 -- cgit v1.2.3 From e37115a620ff33d4f0aeb9bac3ced3e8578605e0 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 18 Oct 2021 22:53:54 -0400 Subject: finished basic question interface --- bot/exts/events/trivianight/questions.py | 56 +++++++++++++++++++++++++++---- bot/exts/events/trivianight/scoreboard.py | 2 +- 2 files changed, 51 insertions(+), 7 deletions(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/questions.py b/bot/exts/events/trivianight/questions.py index 21277ce9..2c1dbc81 100644 --- a/bot/exts/events/trivianight/questions.py +++ b/bot/exts/events/trivianight/questions.py @@ -42,12 +42,10 @@ class QuestionButton(Button): class QuestionView(View): """View for the questions.""" - def __init__(self, scoreboard: Scoreboard): - self.scoreboard = scoreboard + def __init__(self): self.current_question = {} - self._users_picked = {} - def _create_current_question(self) -> Embed: + 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']}", @@ -58,14 +56,60 @@ class QuestionView(View): 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) -> dict: + 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.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 return_dict + return answer_embed diff --git a/bot/exts/events/trivianight/scoreboard.py b/bot/exts/events/trivianight/scoreboard.py index 34535690..27a45e30 100644 --- a/bot/exts/events/trivianight/scoreboard.py +++ b/bot/exts/events/trivianight/scoreboard.py @@ -74,7 +74,7 @@ class Scoreboard: if item.startswith("points: "): return self.view.points[item.removeprefix("points: ")] elif item.startswith("speed: "): - return self.view.speed[item.removepreix("speed: ")] + return self.view.speed[item.removeprefix("speed: ")] def display(self) -> Union[Embed, View]: """Returns the embed of the main leaderboard along with the ScoreboardView.""" -- cgit v1.2.3 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 +++++++++++++++++++++++++++++ bot/exts/events/trivianight/_scoreboard.py | 81 ++++++++++++++++++++ bot/exts/events/trivianight/questions.py | 115 ---------------------------- bot/exts/events/trivianight/scoreboard.py | 81 -------------------- bot/exts/events/trivianight/trivianight.py | 27 +++++++ 5 files changed, 224 insertions(+), 196 deletions(-) create mode 100644 bot/exts/events/trivianight/_questions.py create mode 100644 bot/exts/events/trivianight/_scoreboard.py delete mode 100644 bot/exts/events/trivianight/questions.py delete mode 100644 bot/exts/events/trivianight/scoreboard.py (limited to 'bot') 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 diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py new file mode 100644 index 00000000..96ff5ced --- /dev/null +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -0,0 +1,81 @@ +from typing import Union + +import discord.ui +from discord import ButtonStyle, Embed, Interaction +from discord.ui import Button, View + +from bot.bot import Bot +from bot.constants import Colours + + +class ScoreboardView(View): + """View for the scoreboard.""" + + def __init__(self, bot: Bot): + self.bot = bot + self.points = {} + self.speed = {} + + 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) + + return main_embed + + 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]: + speed_embed.add_field( + name=self.bot.get_user(user), + value=f"`{(time_taken[1] / time_taken[0]):.3f}s` (on average)", + inline=False + ) + + return speed_embed + + @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) + + +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: ") + 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: ")] + + 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 diff --git a/bot/exts/events/trivianight/questions.py b/bot/exts/events/trivianight/questions.py deleted file mode 100644 index 2c1dbc81..00000000 --- a/bot/exts/events/trivianight/questions.py +++ /dev/null @@ -1,115 +0,0 @@ -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.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 diff --git a/bot/exts/events/trivianight/scoreboard.py b/bot/exts/events/trivianight/scoreboard.py deleted file mode 100644 index 27a45e30..00000000 --- a/bot/exts/events/trivianight/scoreboard.py +++ /dev/null @@ -1,81 +0,0 @@ -from typing import Union - -import discord.ui -from discord import ButtonStyle, Embed, Interaction -from discord.ui import Button, View - -from bot.bot import Bot -from bot.constants import Colours - - -class ScoreboardView(View): - """View for the scoreboard.""" - - def __init__(self, bot: Bot): - self.bot = bot - self.points = {} - self.speed = {} - - 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) - - return main_embed - - 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]: - speed_embed.add_field( - name=self.bot.get_user(user), - value=f"`{(time_taken[1] / time_taken[0]):.3f}s` (on average)", - inline=False - ) - - return speed_embed - - @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) - - -class Scoreboard: - """Class for the scoreboard for the trivianight event.""" - - def __init__(self): - self.view = ScoreboardView() - - 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: ")] - - 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 diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 29a9e3d1..66b2ae43 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -1,6 +1,13 @@ +from json import loads +from random import choice + +from discord import Embed 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 class TriviaNight(commands.Cog): @@ -8,6 +15,26 @@ class TriviaNight(commands.Cog): def __init__(self, bot: Bot): self.bot = bot + self.scoreboard = Scoreboard(self.bot) + self.questions = Questions(self.scoreboard) + + @commands.group() + async def trivianight(self, ctx: commands.Context) -> None: + """No-op subcommand group for organizing different commands.""" + return + + @trivianight.command() + async def load(self, ctx: commands.Context) -> None: + """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.set_questions(serialized_json) + success_embed = Embed( + title=choice(POSITIVE_REPLIES), + description="The JSON was loaded successfully!", + color=Colours.soft_green + ) + await ctx.send(embed=success_embed) def setup(bot: Bot) -> None: -- 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') 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') 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 2ae3deeccaafc8cabe5263decc8ce3c69db970d1 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sat, 23 Oct 2021 17:17:51 -0400 Subject: added ranks as an ephemeral and redid scoreboard --- bot/exts/events/trivianight/_scoreboard.py | 79 +++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 18 deletions(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 7eb7a6a8..d3e1bfb3 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -1,11 +1,12 @@ +from random import choice from typing import Union import discord.ui -from discord import ButtonStyle, Embed, Interaction +from discord import ButtonStyle, Embed, Interaction, Member from discord.ui import Button, View from bot.bot import Bot -from bot.constants import Colours +from bot.constants import Colours, NEGATIVE_REPLIES class ScoreboardView(View): @@ -25,43 +26,85 @@ class ScoreboardView(View): color=Colours.python_blue, ) + formatted_string = "" + participant_points = list(self.points.items())[:30] if len(self.points.items()) > 30 else self.points.items() current_placement = 1 - for user, points in self.points.items(): + for user, points in participant_points: user = await self.bot.fetch_user(int(user)) - main_embed.add_field( - name=f"{current_placement}. {user.name}#{user.discriminator}", - value=f"`{points}` pts" - ) + formatted_string += f"`{current_placement}`. {user.mention} " + formatted_string += f"({points} pts)\n" + if current_placement % 10 == 0: + formatted_string += "⎯⎯⎯⎯⎯⎯⎯⎯\n" current_placement += 1 return main_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, - ) current_placement = 1 - for user, time_taken in self.speed.items(): + formatted_string = "" + participant_speed = list(self.speed.items())[:30] if len(self.speed.items()) > 30 else self.speed.items() + + for user, time_taken in participant_speed: user = await self.bot.fetch_user(int(user)) - speed_embed.add_field( - name=f"{current_placement}. {user.name}#{user.discriminator}", - value=f"`{(time_taken[1] / time_taken[0]):.3f}s` (on average)", - ) + formatted_string += f"`{current_placement}`. {user.mention} " + formatted_string += f"({time_taken:.1f}s)\n" + if current_placement % 10 == 0: + formatted_string += "⎯⎯⎯⎯⎯⎯⎯⎯\n" current_placement += 1 + speed_embed = Embed( + title="Average time taken to answer a question", + description=formatted_string, + color=Colours.python_blue + ) return speed_embed + 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) + 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) + except ValueError: + return Embed( + title=choice(NEGATIVE_REPLIES), + description="It looks like you didn't participate in the Trivia Night event!", + color=Colours.soft_red + ) + + suffixes = {"1": "st", "2": "nd", "3": "rd"} + rank_embed.add_field( + 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." + ), + inline=False + ) + rank_embed.add_field( + 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." + ), + inline=False + ) + return rank_embed + @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=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.""" + await interaction.response.send_message(embed=self._get_rank(interaction.user), ephemeral=True) + class Scoreboard: - """Class for the scoreboard for the trivianight event.""" + """Class for the scoreboard for the Trivia Night event.""" def __setitem__(self, key: str, value: int): if key.startswith("points: "): -- cgit v1.2.3 From cfae0a04156fbde062fa8847f85f16de49cd3a83 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sat, 23 Oct 2021 23:45:54 -0400 Subject: using enumerate for brevity --- bot/exts/events/trivianight/_scoreboard.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index d3e1bfb3..c39fc666 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -28,12 +28,11 @@ class ScoreboardView(View): formatted_string = "" participant_points = list(self.points.items())[:30] if len(self.points.items()) > 30 else self.points.items() - current_placement = 1 - for user, points in participant_points: + for current_placement, (user, points) in participant_points: user = await self.bot.fetch_user(int(user)) - formatted_string += f"`{current_placement}`. {user.mention} " + formatted_string += f"`{current_placement + 1}`. {user.mention} " formatted_string += f"({points} pts)\n" - if current_placement % 10 == 0: + if (current_placement + 1) % 10 == 0: formatted_string += "⎯⎯⎯⎯⎯⎯⎯⎯\n" current_placement += 1 @@ -41,15 +40,14 @@ class ScoreboardView(View): async def _create_speed_embed(self) -> Embed: """Helper function that iterates through `self.speed` to generate a leaderboard embed.""" - current_placement = 1 formatted_string = "" participant_speed = list(self.speed.items())[:30] if len(self.speed.items()) > 30 else self.speed.items() - for user, time_taken in participant_speed: + for current_placement, (user, time_taken) in enumerate(participant_speed): user = await self.bot.fetch_user(int(user)) - formatted_string += f"`{current_placement}`. {user.mention} " + formatted_string += f"`{current_placement + 1}`. {user.mention} " formatted_string += f"({time_taken:.1f}s)\n" - if current_placement % 10 == 0: + if (current_placement + 1) % 10 == 0: formatted_string += "⎯⎯⎯⎯⎯⎯⎯⎯\n" current_placement += 1 -- 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') 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') 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') 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') 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') 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') 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') 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') 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') 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') 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') 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') 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 4b999a506d1736d636d298b3316686014f4630fb Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Thu, 4 Nov 2021 16:08:03 -0400 Subject: better docstring for load --- bot/exts/events/trivianight/trivianight.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 46db8c74..96493f2f 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -45,7 +45,22 @@ class TriviaNight(commands.Cog): @trivianight.command() async def load(self, ctx: commands.Context, *, to_load: Optional[str]) -> None: - """Load the JSON file provided into the questions.""" + """ + Loads a JSON file from the provided attachment or argument. + + 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) + - 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) + + The load command accepts three different ways of loading in a JSON: + - an attachment of the JSON file + - a message link to the attachment/JSON + - reading the JSON itself via a codeblock or plain text + """ if ctx.message.attachments: json_text = (await ctx.message.attachments[0].read()).decode("utf8") elif to_load.startswith("https://discord.com/channels") or \ -- 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') 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 73200817a80c19f48ab8c5af74e004d6c81841d5 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sat, 6 Nov 2021 11:20:57 -0400 Subject: more bug fixes/sorting --- bot/exts/events/trivianight/_scoreboard.py | 3 +++ bot/exts/events/trivianight/trivianight.py | 4 +--- 2 files changed, 4 insertions(+), 3 deletions(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 076fd406..e9678bb8 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -21,6 +21,9 @@ class ScoreboardView(View): async def create_main_leaderboard(self) -> Embed: """Helper function that iterates through `self.points` to generate the main leaderboard embed.""" 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])) + for current_placement, (user, points) in enumerate(self.points.items()): if current_placement + 1 > 30: break diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 96493f2f..75d2c671 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -71,9 +71,7 @@ class TriviaNight(commands.Cog): 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", "") + json_text = message.content.replace("```", "").replace("json", "").replace("\n", "") try: serialized_json = loads(json_text) -- 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') 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') 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 c0917e9de25321f1d533568a0abe2bef8ee8c91b Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 16 Nov 2021 18:39:14 -0500 Subject: fixing rivianight reset bugs buttons would be repeated due to a faulty reset --- bot/exts/events/trivianight/_scoreboard.py | 4 ++-- bot/exts/events/trivianight/trivianight.py | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index e9678bb8..08025214 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -113,12 +113,12 @@ class Scoreboard: def __setitem__(self, key: str, value: int): if value.get("points") and key.user_id not in self.view.points.keys(): self.view.points[key.user_id] = value["points"] - else: + 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"]] - else: + 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"] ] diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 75d2c671..224b0620 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -94,13 +94,18 @@ class TriviaNight(commands.Cog): 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: + all_questions = self.questions.questions + self.questions.view = QuestionView() + + for question in all_questions: if "visited" in question.keys(): del question["visited"] + self.questions.questions = list(all_questions) + success_embed = Embed( title=choice(POSITIVE_REPLIES), - description="The scoreboards were reset and questions marked unvisited!", + description="The scoreboards were reset and questions reset!", color=Colours.soft_green ) await ctx.send(embed=success_embed) -- 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') 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 450da462d2b9ee3c74fb8f79a9f1bc523fecf8bb Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Wed, 17 Nov 2021 17:55:21 -0500 Subject: add command role checks to moderation roles + trivia night roles --- bot/exts/events/trivianight/trivianight.py | 33 ++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 11 deletions(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 615a9dd3..d00a70aa 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -7,11 +7,14 @@ from discord import Embed from discord.ext import commands from bot.bot import Bot -from bot.constants import Colours, NEGATIVE_REPLIES, POSITIVE_REPLIES +from bot.constants import Colours, MODERATION_ROLES, NEGATIVE_REPLIES, POSITIVE_REPLIES from ._questions import QuestionView, Questions from ._scoreboard import Scoreboard, ScoreboardView +# The ID you see below is the Events Lead role ID +TRIVIA_NIGHT_ROLES = MODERATION_ROLES + (778361735739998228,) + class TriviaNight(commands.Cog): """Cog for the Python Trivia Night event.""" @@ -32,18 +35,18 @@ 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.""" - 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) + 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() + @commands.has_any_role(*TRIVIA_NIGHT_ROLES) async def load(self, ctx: commands.Context, *, to_load: Optional[str]) -> None: """ Loads a JSON file from the provided attachment or argument. @@ -63,6 +66,8 @@ class TriviaNight(commands.Cog): """ if ctx.message.attachments: json_text = (await ctx.message.attachments[0].read()).decode("utf8") + elif not to_load: + raise commands.BadArgument("You didn't attach an attachment nor link a message!") elif to_load.startswith("https://discord.com/channels") or \ to_load.startswith("https://discordapp.com/channels"): channel_id, message_id = to_load.split("/")[-2:] @@ -92,6 +97,7 @@ 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.""" self.scoreboard.view = ScoreboardView(self.bot) @@ -112,6 +118,7 @@ class TriviaNight(commands.Cog): await ctx.send(embed=success_embed) @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.""" if self.questions.view.active_question is True: @@ -145,6 +152,7 @@ class TriviaNight(commands.Cog): 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.""" question = self.questions.next_question(question_number) @@ -169,6 +177,7 @@ class TriviaNight(commands.Cog): await message.edit(embed=question_embed, view=None) @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.""" question_list = self.questions.list_questions() @@ -178,6 +187,7 @@ class TriviaNight(commands.Cog): await ctx.send(question_list) @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.""" if self.questions.view.active_question is False: @@ -192,6 +202,7 @@ class TriviaNight(commands.Cog): self.questions.view.active_question = False @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.""" scoreboard_embed, scoreboard_view = await self.scoreboard.display() -- cgit v1.2.3 From 9b8bd12fa3b56b76fa6f8c759597e0f845d641c3 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Wed, 17 Nov 2021 18:00:37 -0500 Subject: setup function --- bot/exts/events/trivianight/trivianight.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index d00a70aa..bee55670 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -24,6 +24,17 @@ class TriviaNight(commands.Cog): 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.""" @@ -86,9 +97,8 @@ class TriviaNight(commands.Cog): for idx, question in enumerate(serialized_json): 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) + self.setup_views(serialized_json) + success_embed = Embed( title=choice(POSITIVE_REPLIES), description="The JSON was loaded successfully!", @@ -100,15 +110,13 @@ class TriviaNight(commands.Cog): @commands.has_any_role(*TRIVIA_NIGHT_ROLES) async def reset(self, ctx: commands.Context) -> None: """Resets previous questions and scoreboards.""" - self.scoreboard.view = ScoreboardView(self.bot) all_questions = self.questions.questions - self.questions.view = QuestionView() for question in all_questions: if "visited" in question.keys(): del question["visited"] - self.questions.set_questions(list(all_questions)) + self.setup_views(list(all_questions)) success_embed = Embed( title=choice(POSITIVE_REPLIES), -- cgit v1.2.3 From afe61522fdb8c6592b251476aef0d6823676c176 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Wed, 17 Nov 2021 18:03:35 -0500 Subject: locking trivia night roles to admins and event lead only --- bot/exts/events/trivianight/trivianight.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index bee55670..ed2bfdbe 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -7,13 +7,13 @@ from discord import Embed from discord.ext import commands from bot.bot import Bot -from bot.constants import Colours, MODERATION_ROLES, NEGATIVE_REPLIES, POSITIVE_REPLIES +from bot.constants import Colours, NEGATIVE_REPLIES, POSITIVE_REPLIES, Roles from ._questions import QuestionView, Questions from ._scoreboard import Scoreboard, ScoreboardView # The ID you see below is the Events Lead role ID -TRIVIA_NIGHT_ROLES = MODERATION_ROLES + (778361735739998228,) +TRIVIA_NIGHT_ROLES = (Roles.admin, 78361735739998228) class TriviaNight(commands.Cog): -- 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') 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 52f1b8cbcc41950f9845d4ef1026253691b921c7 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 28 Nov 2021 22:46:29 -0500 Subject: add logic to make sure that the event can't be ended during a question --- bot/exts/events/trivianight/trivianight.py | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'bot') diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index a86bd73f..1fffe9fe 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -254,6 +254,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: + error_embed = Embed( + title=choice(NEGATIVE_REPLIES), + description="You can't end the event while a question is ongoing!", + color=Colours.soft_red + ) + await ctx.send(embed=error_embed) + return + scoreboard_embed, scoreboard_view = await self.scoreboard.display() await ctx.send(embed=scoreboard_embed, view=scoreboard_view) -- cgit v1.2.3 From 5d84b7947dbaee89e91ed4e8743b7bee3ec9b677 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 29 Nov 2021 18:36:59 -0500 Subject: forgot eturn after error message --- bot/exts/events/trivianight/trivianight.py | 1 + 1 file changed, 1 insertion(+) (limited to 'bot') diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 1fffe9fe..981b6937 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -220,6 +220,7 @@ class TriviaNight(commands.Cog): question_list = self.questions.list_questions() if isinstance(question_list, Embed): await ctx.send(embed=question_list) + return await ctx.send(question_list) -- 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') 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 176590740ed21f8faa26348ab710ee722fca6926 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 29 Nov 2021 19:41:33 -0500 Subject: typo in trivianight cog explanation --- bot/exts/events/trivianight/trivianight.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 981b6937..59b809b1 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -59,7 +59,7 @@ class TriviaNight(commands.Cog): 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!" + " players in a trivia about Python!" ), color=Colours.soft_green ) -- cgit v1.2.3 From 2a7d6942646f9cd0c71d3f1e5812dd1e49043be8 Mon Sep 17 00:00:00 2001 From: Bluenix Date: Sun, 12 Dec 2021 18:00:15 +0100 Subject: Add Question representation for trivia night data --- bot/exts/events/trivianight/_game.py | 105 +++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 bot/exts/events/trivianight/_game.py (limited to 'bot') diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py new file mode 100644 index 00000000..086d0de6 --- /dev/null +++ b/bot/exts/events/trivianight/_game.py @@ -0,0 +1,105 @@ +import time +from string import ascii_uppercase +from typing import Iterable, Optional, TypedDict + + +DEFAULT_QUESTION_POINTS = 10 +DEFAULT_QUESTION_TIME = 10 + + +class QuestionData(TypedDict): + """Representing the different 'keys' of the question taken from the JSON.""" + + number: str + description: str + answers: list[str] + correct: str + points: Optional[int] + time: Optional[int] + + +UserGuess = tuple[ + str, # The answer that was guessed + bool, # Whether the answer can be changed again + float # The time it took to guess +] + + +class Question: + """Interface for one question in a trivia night game.""" + + def __init__(self, data: QuestionData): + self._data = data + self._guesses: dict[int, UserGuess] = {} + self._started = None + + # These properties are mostly proxies to the underlying data: + + @property + def number(self) -> str: + """The number of the question.""" + return self._data["number"] + + @property + def description(self) -> str: + """The description of the question.""" + return self._data["description"] + + @property + def answers(self) -> list[tuple[str, str]]: + """The possible answers for this answer. + + This is a property that returns a list of letter, answer pairs. + """ + return [(ascii_uppercase[i], q) for (i, q) in enumerate(self._data["answers"])] + + @property + def correct(self) -> str: + """The correct answer for this question.""" + return self._data["correct"] + + @property + def max_points(self) -> int: + """The maximum points that can be awarded for this question.""" + return self._data.get("points") or DEFAULT_QUESTION_POINTS + + @property + def time(self) -> float: + """The time allowed to answer the question.""" + return self._data.get("time") or DEFAULT_QUESTION_TIME + + def start(self) -> float: + """Start the question and return the time it started.""" + self._started = time.perf_counter() + return self._started + + 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.") + + if self._guesses[user][1] is False: + raise RuntimeError(f"User({user}) has already updated their guess once.") + + self._guesses[user] = (answer, False, time.perf_counter() - self._started) + return self._guesses[user] + + def guess(self, user: int, answer: str) -> UserGuess: + """Add a guess made by a user to the current question.""" + if user in self._guesses: + return self._update_guess(user, answer) + + if self._started is None: + raise RuntimeError("Question is not open for answers.") + + self._guesses[user] = (answer, True, time.perf_counter() - self._started) + return self._guesses[user] + + def stop(self) -> dict[int, UserGuess]: + """Stop the question and return the guesses that were made.""" + guesses = self._guesses + + self._started = None + self._guesses = {} + + return guesses -- 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') 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') 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 7e8b7335ed02897a85eac97e10f10a0c21658b15 Mon Sep 17 00:00:00 2001 From: Bluenix Date: Sun, 19 Dec 2021 22:22:44 +0100 Subject: Fix bugs after testing and add TODO comments --- bot/exts/events/trivianight/_game.py | 1 - bot/exts/events/trivianight/trivianight.py | 38 ++++++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 6 deletions(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index aac745a7..7f2e48dc 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -142,7 +142,6 @@ class TriviaNightGame: question = self._questions.pop(randrange(len(self._questions))) self.current_question = question - self.current_question.start() return question def end_question(self) -> None: diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 86da0c3a..1465a03d 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -11,7 +11,7 @@ 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 ._questions import QuestionView from ._scoreboard import Scoreboard, ScoreboardView # The ID you see below is the Events Lead role ID @@ -152,7 +152,8 @@ class TriviaNightCog(commands.Cog): await ctx.send(embed=question_view.end_question()) await message.edit(embed=question_embed, view=None) - question_view.stop() + + self.game.end_question() @trivianight.command() @commands.has_any_role(*TRIVIA_NIGHT_ROLES) @@ -163,6 +164,21 @@ class TriviaNightCog(commands.Cog): Questions are displayed in the following format: Q(number): Question description | :white_check_mark: if the question was used otherwise :x:. """ + 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 + + # 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() if isinstance(question_list, Embed): await ctx.send(embed=question_list) @@ -178,16 +194,27 @@ class TriviaNightCog(commands.Cog): 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: + 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 None: error_embed = Embed( title=choice(NEGATIVE_REPLIES), - description="There is not an ongoing question to stop!", + description="There is no ongoing question!", color=Colours.soft_red ) await ctx.send(embed=error_embed) return - self.questions.view.active_question = False + # 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() @trivianight.command() @commands.has_any_role(*TRIVIA_NIGHT_ROLES) @@ -218,6 +245,7 @@ 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 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') 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 a50739aac6144022a481d78c637b5d5fd7769913 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 21 Dec 2021 15:59:08 -0500 Subject: full refactor complete structure overhauled, changed stop logic --- bot/exts/events/trivianight/trivianight.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index f158ec0c..18e0dce1 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -144,7 +144,15 @@ class TriviaNightCog(commands.Cog): percentage *= 0.5 duration = next_question.time * percentage - await asyncio.sleep(duration) + await asyncio.wait([self.question_closed.wait()], timeout=duration) + + if self.question_closed.is_set(): + await ctx.send(embed=question_view.end_question(self.scoreboard)) + await message.edit(embed=question_embed, view=None) + + self.game.end_question() + self.question_closed.clear() + return if int(duration) > 1: # It is quite ugly to display decimals, the delay for requests to reach Discord @@ -153,7 +161,14 @@ class TriviaNightCog(commands.Cog): 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) + await asyncio.wait([self.question_closed.wait()], timeout=duration) + if self.question_closed.is_set(): + await ctx.send(embed=question_view.end_question(self.scoreboard)) + await message.edit(embed=question_embed, view=None) + + self.game.end_question() + self.question_closed.clear() + return break await ctx.send(embed=question_view.end_question(self.scoreboard)) @@ -210,7 +225,7 @@ class TriviaNightCog(commands.Cog): await ctx.send(embed=error_embed) return - self.ongoing_question = False + self.question_closed.set() @trivianight.command() @commands.has_any_role(*TRIVIA_NIGHT_ROLES) -- 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') 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 f48a03318b9a67563807f0a081430026ecfe8419 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 28 Dec 2021 00:30:51 -0500 Subject: fix \ with if --- bot/exts/events/trivianight/trivianight.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 18e0dce1..0971ea2a 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -74,8 +74,10 @@ class TriviaNightCog(commands.Cog): json_text = (await ctx.message.attachments[0].read()).decode("utf8") elif not to_load: raise commands.BadArgument("You didn't attach an attachment nor link a message!") - elif to_load.startswith("https://discord.com/channels") or \ - to_load.startswith("https://discordapp.com/channels"): + 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)) -- cgit v1.2.3 From e9f65c805ca16cdc1bfb2e13f6a6c91d9f511480 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 28 Dec 2021 00:31:47 -0500 Subject: display error when something goes wrong with .trivianight load --- bot/exts/events/trivianight/trivianight.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 0971ea2a..aa6b8967 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -90,8 +90,8 @@ class TriviaNightCog(commands.Cog): try: serialized_json = loads(json_text) - except JSONDecodeError: - raise commands.BadArgument("Invalid JSON") + except JSONDecodeError as error: + raise commands.BadArgument(f"Looks like something went wrong:\n{str(error)}") self.game = TriviaNightGame(serialized_json) self.question_closed = asyncio.Event() -- cgit v1.2.3 From 736cddce6b84cd8df09da1904c20e503dc4a46e1 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 28 Dec 2021 00:37:50 -0500 Subject: improving ordinal number usage in _scoreboard.ScoreboardView._get_rank --- bot/exts/events/trivianight/_scoreboard.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index d9107dca..6f95f22c 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -91,19 +91,31 @@ class ScoreboardView(View): color=Colours.soft_red ) - suffixes = {"1": "st", "2": "nd", "3": "rd"} + suffix = ["th", "st", "nd", "rd", "th"][min(int(points_rank) % 10, 4)] + if (int(points_rank) % 100) in {11, 12, 13}: + suffix = "th" + + points_rank = str(points_rank) + suffix + rank_embed.add_field( name="Total Points", value=( - f"You got {points_rank}{'th' if not (suffix := suffixes.get(points_rank[-1])) else suffix} place" + f"You got {points_rank} place" f" with {self.points[member.id]:.1f} points." ), inline=False ) + + suffix = ["th", "st", "nd", "rd", "th"][min(int(speed_rank) % 10, 4)] + if (int(speed_rank) % 100) in {11, 12, 13}: + suffix = "th" + + speed_rank = str(speed_rank) + suffix + rank_embed.add_field( name="Average Speed", value=( - f"You got {speed_rank}{'th' if not (suffix := suffixes.get(speed_rank[-1])) else suffix} place" + f"You got {speed_rank} place" f" with a time of {(self.speed[member.id][1] / self.speed[member.id][0]):.1f} seconds." ), inline=False -- cgit v1.2.3 From f436e87e2a3d4a2150293090d8236068b21d9257 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 9 Jan 2022 17:21:04 -0500 Subject: fix visited for .tn list not working --- bot/exts/events/trivianight/_game.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index db303c58..0b5fe562 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -178,6 +178,6 @@ class TriviaNightGame: 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" + f" {visited if question not in self._questions else not_visited}\n" return formatted_string -- 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') 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 77a20ef8008d0815c3250433c88b5096ae843fa4 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 9 Jan 2022 17:46:56 -0500 Subject: sort the speed leaderboard properly --- bot/exts/events/trivianight/_scoreboard.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 6f95f22c..bf744389 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -24,8 +24,6 @@ class ScoreboardView(View): 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])) for current_placement, (user, points) in enumerate(self.points.items()): if current_placement + 1 > 30: @@ -175,6 +173,7 @@ class Scoreboard: 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 + self.view.points = dict(sorted(self._points.items(), key=lambda item: item[-1], reverse=True)) + self.view.speed = dict(sorted(self._speed.items(), key=lambda item: item[-1][1] / item[-1][0])) + return await self.view.create_main_leaderboard(), self.view -- cgit v1.2.3 From 60a1747edfee370fb9a7af86d4c13118f665c65e Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 9 Jan 2022 17:55:07 -0500 Subject: preventing the number of points from reaching too high --- bot/exts/events/trivianight/_scoreboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index bf744389..769d6a88 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -162,7 +162,7 @@ class Scoreboard: 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_id] += self._points[user_id] + self._points[user_id] += points if speed is not None and user_id not in self._speed.keys(): self._speed[user_id] = [1, speed] -- 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') 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') 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 b68302524628573ee7e20ccd81db6bb60c05061b Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 9 Jan 2022 18:26:08 -0500 Subject: fixing .tn next logic --- bot/exts/events/trivianight/_game.py | 8 +++++++- bot/exts/events/trivianight/trivianight.py | 17 +++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index 9d8b98c1..7f667dcf 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -33,6 +33,10 @@ class AlreadyUpdated(RuntimeError): """Exception raised when the user has already updated their guess once.""" +class AllQuestionsVisited(RuntimeError): + """Exception raised when all of the questions have been visited.""" + + class Question: """Interface for one question in a trivia night game.""" @@ -142,6 +146,8 @@ class TriviaNightGame: question = [q for q in self._all_questions if q.number == number][0] except IndexError: raise ValueError(f"Question number {number} does not exist.") + elif len(self._questions) == 0: + raise AllQuestionsVisited("All of the questions have been visited.") else: question = self._questions.pop(randrange(len(self._questions))) @@ -161,7 +167,7 @@ class TriviaNightGame: self.current_question.stop() self.current_question = None - def list_questions(self) -> None: + def list_questions(self) -> str: """ List all the questions. diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index aa6b8967..af517260 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -9,7 +9,7 @@ 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 ._game import AllQuestionsVisited, TriviaNightGame from ._questions import QuestionView from ._scoreboard import Scoreboard @@ -132,7 +132,16 @@ class TriviaNightCog(commands.Cog): await ctx.send(embed=error_embed) return - next_question = self.game.next_question(question_number) + try: + next_question = self.game.next_question(question_number) + except AllQuestionsVisited: + error_embed = Embed( + title=choice(NEGATIVE_REPLIES), + description="All of the questions have been used.", + color=Colours.soft_red + ) + await ctx.send(embed=error_embed) + return question_view = QuestionView(next_question) question_embed = question_view.create_embed() @@ -196,10 +205,6 @@ class TriviaNightCog(commands.Cog): return question_list = self.game.list_questions() - if isinstance(question_list, Embed): - await ctx.send(embed=question_list) - return - await ctx.send(question_list) @trivianight.command() -- 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') 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 4497ec3c52fe17427567e8e81eeb618a61c7dd24 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 10 Jan 2022 21:55:11 -0500 Subject: 3 second countdown before question starts. --- bot/exts/events/trivianight/trivianight.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'bot') diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index af517260..6676a14e 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -143,6 +143,9 @@ class TriviaNightCog(commands.Cog): await ctx.send(embed=error_embed) return + await ctx.send("Next question in 3 seconds! Get ready...") + await asyncio.sleep(3) + question_view = QuestionView(next_question) question_embed = question_view.create_embed() -- 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') 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 From cffb97531a4fe1ccd3df7d7d0bdcc57ed749c16b Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 10 Jan 2022 22:06:03 -0500 Subject: default timer is at 20 seconds --- bot/exts/events/trivianight/_game.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index 7f667dcf..994006bf 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -4,7 +4,7 @@ from string import ascii_uppercase from typing import Iterable, Optional, TypedDict DEFAULT_QUESTION_POINTS = 10 -DEFAULT_QUESTION_TIME = 10 +DEFAULT_QUESTION_TIME = 20 class QuestionData(TypedDict): -- cgit v1.2.3 From de66d41a7ab350b684ed7b5f829136ebdb4d9c37 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 10 Jan 2022 22:09:23 -0500 Subject: Apply suggestions from code review Co-authored-by: Johannes Christ --- bot/exts/events/trivianight/_game.py | 9 ++++----- bot/exts/events/trivianight/_scoreboard.py | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index 994006bf..a47025c2 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -18,11 +18,10 @@ class QuestionData(TypedDict): time: Optional[int] -UserGuess = tuple[ - str, # The answer that was guessed - bool, # Whether the answer can be changed again - float # The time it took to guess -] +class UserGuess(NamedTuple): + answer: str + editable: bool + elapsed: float class QuestionClosed(RuntimeError): diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 769d6a88..583532a3 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -80,8 +80,8 @@ class ScoreboardView(View): 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(member.id) + 1) - speed_rank = str(list(self.speed.keys()).index(member.id) + 1) + points_rank = str(list(self.points).index(member.id) + 1) + speed_rank = str(list(self.speed).index(member.id) + 1) except ValueError: return Embed( title=choice(NEGATIVE_REPLIES), -- cgit v1.2.3 From 7af2e3d215962d613566416cdc69457b332ecb20 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 10 Jan 2022 22:14:28 -0500 Subject: int to ordinal as a separate method --- bot/exts/events/trivianight/_scoreboard.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 583532a3..56a86029 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -15,6 +15,15 @@ class ScoreboardView(View): super().__init__() self.bot = bot + @staticmethod + def _int_to_ordinal(number: int) -> str: + """Converts an integer into an ordinal number, i.e. 1 to 1st.""" + suffix = ["th", "st", "nd", "rd", "th"][min(number % 10, 4)] + if (number % 100) in {11, 12, 13}: + suffix = "th" + + return str(number) + suffix + async def create_main_leaderboard(self) -> Embed: """ Helper function that iterates through `self.points` to generate the main leaderboard embed. @@ -89,31 +98,19 @@ class ScoreboardView(View): color=Colours.soft_red ) - suffix = ["th", "st", "nd", "rd", "th"][min(int(points_rank) % 10, 4)] - if (int(points_rank) % 100) in {11, 12, 13}: - suffix = "th" - - points_rank = str(points_rank) + suffix - rank_embed.add_field( name="Total Points", value=( - f"You got {points_rank} place" + f"You got {self._int_to_ordinal(int(points_rank))} place" f" with {self.points[member.id]:.1f} points." ), inline=False ) - suffix = ["th", "st", "nd", "rd", "th"][min(int(speed_rank) % 10, 4)] - if (int(speed_rank) % 100) in {11, 12, 13}: - suffix = "th" - - speed_rank = str(speed_rank) + suffix - rank_embed.add_field( name="Average Speed", value=( - f"You got {speed_rank} place" + f"You got {self._int_to_ordinal(int(speed_rank))} place" f" with a time of {(self.speed[member.id][1] / self.speed[member.id][0]):.1f} seconds." ), inline=False -- cgit v1.2.3 From 74dcb9910da45c4e135ece34f6491c625bba6a79 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Tue, 11 Jan 2022 20:00:13 -0500 Subject: reverting namedtuple change --- bot/exts/events/trivianight/_game.py | 9 +++++---- bot/exts/events/trivianight/_scoreboard.py | 7 ++++++- 2 files changed, 11 insertions(+), 5 deletions(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index a47025c2..1526aa14 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -18,10 +18,11 @@ class QuestionData(TypedDict): time: Optional[int] -class UserGuess(NamedTuple): - answer: str - editable: bool - elapsed: float +UserGuess = tuple[ + str, # Represents the answer choice the user chose. + bool, # Represents if the user can edit their answer. + float # Represents the amount of time passed since the question began. +] class QuestionClosed(RuntimeError): diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 56a86029..7ec3c76b 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -17,7 +17,12 @@ class ScoreboardView(View): @staticmethod def _int_to_ordinal(number: int) -> str: - """Converts an integer into an ordinal number, i.e. 1 to 1st.""" + """ + Converts an integer into an ordinal number, i.e. 1 to 1st. + + Parameters: + - number: an integer representing the number to convert to an ordinal number. + """ suffix = ["th", "st", "nd", "rd", "th"][min(number % 10, 4)] if (number % 100) in {11, 12, 13}: suffix = "th" -- cgit v1.2.3 From 4e5f98468d8fc450e01c99450f43f0c77b543747 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Wed, 12 Jan 2022 15:40:48 -0500 Subject: reverting back to original commit for a NamedTuple --- bot/exts/events/trivianight/_game.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index 1526aa14..6d177783 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -1,7 +1,7 @@ import time from random import randrange from string import ascii_uppercase -from typing import Iterable, Optional, TypedDict +from typing import Iterable, NamedTuple, Optional, TypedDict DEFAULT_QUESTION_POINTS = 10 DEFAULT_QUESTION_TIME = 20 @@ -18,11 +18,12 @@ class QuestionData(TypedDict): time: Optional[int] -UserGuess = tuple[ - str, # Represents the answer choice the user chose. - bool, # Represents if the user can edit their answer. - float # Represents the amount of time passed since the question began. -] +class UserGuess(NamedTuple): + """Represents the user's guess for a question.""" + + answer: str + editable: bool + elapsed: float class QuestionClosed(RuntimeError): -- cgit v1.2.3 From e015fff0a1147a2a2b00a47ca7a465bc9f0d89e3 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Fri, 28 Jan 2022 22:50:19 -0500 Subject: fix question bug --- bot/exts/events/trivianight/_game.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index 6d177783..4b115086 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -144,7 +144,7 @@ class TriviaNightGame: if number is not None: try: - question = [q for q in self._all_questions if q.number == number][0] + question = [q for q in self._all_questions if q.number == int(number)][0] except IndexError: raise ValueError(f"Question number {number} does not exist.") elif len(self._questions) == 0: -- cgit v1.2.3 From 13aea5abca919a4b6105dcfc31b95d14aded5430 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sun, 30 Jan 2022 20:32:58 -0500 Subject: initialize scoreboard view only during display --- bot/exts/events/trivianight/_scoreboard.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index 7ec3c76b..d0d0a49c 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -151,7 +151,7 @@ class Scoreboard: """Class for the scoreboard for the Trivia Night event.""" def __init__(self, bot: Bot): - self.view = ScoreboardView(bot) + self._bot = bot self._points = {} self._speed = {} @@ -175,7 +175,9 @@ class Scoreboard: async def display(self) -> tuple[Embed, View]: """Returns the embed of the main leaderboard along with the ScoreboardView.""" - self.view.points = dict(sorted(self._points.items(), key=lambda item: item[-1], reverse=True)) - self.view.speed = dict(sorted(self._speed.items(), key=lambda item: item[-1][1] / item[-1][0])) + view = ScoreboardView(self._bot) - return await self.view.create_main_leaderboard(), self.view + view.points = dict(sorted(self._points.items(), key=lambda item: item[-1], reverse=True)) + view.speed = dict(sorted(self._speed.items(), key=lambda item: item[-1][1] / item[-1][0])) + + return await view.create_main_leaderboard(), view -- cgit v1.2.3 From 1f5111a75004c677805a16269e0730200cbfea87 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 31 Jan 2022 19:32:54 -0500 Subject: fix scoreboard bugs --- bot/exts/events/trivianight/_scoreboard.py | 7 +++-- bot/exts/events/trivianight/trivianight.py | 42 +++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index d0d0a49c..a5a5fcac 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -173,11 +173,14 @@ class Scoreboard: self._speed[user_id][0] + 1, self._speed[user_id][1] + speed ] - async def display(self) -> tuple[Embed, View]: + async def display(self, speed_leaderboard: bool = False) -> tuple[Embed, View]: """Returns the embed of the main leaderboard along with the ScoreboardView.""" view = ScoreboardView(self._bot) view.points = dict(sorted(self._points.items(), key=lambda item: item[-1], reverse=True)) view.speed = dict(sorted(self._speed.items(), key=lambda item: item[-1][1] / item[-1][0])) - return await view.create_main_leaderboard(), view + return ( + await view.create_main_leaderboard(), + view if not speed_leaderboard else await view._create_speed_embed() + ) diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 6676a14e..bdcf243a 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -241,7 +241,7 @@ class TriviaNightCog(commands.Cog): @commands.has_any_role(*TRIVIA_NIGHT_ROLES) async def end(self, ctx: commands.Context) -> None: """ - Ends the trivia night event and displays the scoreboard view. + 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. @@ -268,8 +268,48 @@ class TriviaNightCog(commands.Cog): scoreboard_embed, scoreboard_view = await self.scoreboard.display() await ctx.send(embed=scoreboard_embed, view=scoreboard_view) + + @trivianight.command() + @commands.has_any_role(*TRIVIA_NIGHT_ROLES) + async def scoreboard(self, ctx: commands.Context) -> None: + """ + Displays the scoreboard. + + The scoreboard 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. + """ + 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!", + color=Colours.soft_red + ) + await ctx.send(embed=error_embed) + return + + scoreboard_embed, speed_scoreboard = await self.scoreboard.display(speed_leaderboard=True) + await ctx.send(embeds=(scoreboard_embed, speed_scoreboard)) + + @trivianight.command() + @commands.has_any_role(*TRIVIA_NIGHT_ROLES) + async def end_game(self, ctx: commands.Context) -> None: + """Ends the ongoing game.""" self.game = None + await ctx.send(embed=Embed( + title=choice(POSITIVE_REPLIES), + description="The game has been stopped.", + color=Colours.soft_green + )) + def setup(bot: Bot) -> None: """Load the TriviaNight cog.""" -- cgit v1.2.3 From 282ac3dc9b3926b16ade2697de03b61634f9dcdd Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 31 Jan 2022 23:26:50 -0500 Subject: pagination --- bot/exts/events/trivianight/trivianight.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index bdcf243a..cf1e45c3 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -8,6 +8,7 @@ from discord.ext import commands from bot.bot import Bot from bot.constants import Colours, NEGATIVE_REPLIES, POSITIVE_REPLIES, Roles +from bot.utils.pagination import LinePaginator from ._game import AllQuestionsVisited, TriviaNightGame from ._questions import QuestionView @@ -207,8 +208,19 @@ class TriviaNightCog(commands.Cog): )) return - question_list = self.game.list_questions() - await ctx.send(question_list) + question_list = self.game.list_questions().split("\n") + + list_embed = Embed(title="All Trivia Night Questions") + + if len(question_list) <= 5: + list_embed.description = "\n".join(question_list) + await ctx.send(embed=list_embed) + else: + await LinePaginator.paginate( + ("\n".join(question_list[idx:idx+5]) for idx in range(0, len(question_list), 5)), + ctx, + list_embed + ) @trivianight.command() @commands.has_any_role(*TRIVIA_NIGHT_ROLES) -- cgit v1.2.3 From 88a65659d5c45ed6be54f35245168f6e30192015 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 7 Feb 2022 18:52:07 -0500 Subject: fix lists command --- bot/exts/events/trivianight/_game.py | 22 +++++++++++++++------- bot/exts/events/trivianight/trivianight.py | 8 ++++---- 2 files changed, 19 insertions(+), 11 deletions(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index 4b115086..4937e6e3 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -177,14 +177,22 @@ class TriviaNightGame: - Question description - Visited/not visited """ + question_list = [] formatted_string = "" - spaces = max(len(q.description) for q in self._all_questions) + visited = ":white_check_mark:" + not_visited = ":x:" for question in self._all_questions: - 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" - - return formatted_string + formatted_string += ( + f"**Q{question.number}** {not_visited if question in self._questions else visited}" + f"\n{question.description}\n\n" + ) + if question.number % 5 == 0: + question_list.append(formatted_string.rstrip()) + formatted_string = "" + + if formatted_string: + question_list.append(formatted_string) + + return question_list diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index cf1e45c3..d190fc13 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -208,16 +208,16 @@ class TriviaNightCog(commands.Cog): )) return - question_list = self.game.list_questions().split("\n") + question_list = self.game.list_questions() list_embed = Embed(title="All Trivia Night Questions") - if len(question_list) <= 5: - list_embed.description = "\n".join(question_list) + if len(question_list) == 1: + list_embed.description = question_list[0] await ctx.send(embed=list_embed) else: await LinePaginator.paginate( - ("\n".join(question_list[idx:idx+5]) for idx in range(0, len(question_list), 5)), + question_list, ctx, list_embed ) -- cgit v1.2.3 From 817091476e94df9b633a4d2686cdb712f73f6eb9 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Mon, 7 Feb 2022 19:27:17 -0500 Subject: let the paginator do its thing --- bot/exts/events/trivianight/_game.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index 4937e6e3..8b012a17 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -178,21 +178,15 @@ class TriviaNightGame: - Visited/not visited """ question_list = [] - formatted_string = "" visited = ":white_check_mark:" not_visited = ":x:" for question in self._all_questions: - formatted_string += ( + formatted_string = ( f"**Q{question.number}** {not_visited if question in self._questions else visited}" f"\n{question.description}\n\n" ) - if question.number % 5 == 0: - question_list.append(formatted_string.rstrip()) - formatted_string = "" - - if formatted_string: - question_list.append(formatted_string) + question_list.append(formatted_string.rstrip()) return question_list -- cgit v1.2.3 From 6e4d1156faff51d05b53ada0e6c0bb2a56f56533 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Wed, 9 Feb 2022 18:14:26 -0500 Subject: add event runner role id --- bot/exts/events/trivianight/trivianight.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index d190fc13..397ff91a 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -14,8 +14,8 @@ from ._game import AllQuestionsVisited, TriviaNightGame from ._questions import QuestionView from ._scoreboard import Scoreboard -# The ID you see below is the Events Lead role ID -TRIVIA_NIGHT_ROLES = (Roles.admin, 78361735739998228) +# The ID you see below are the Events Lead role ID and the Event Runner Role ID +TRIVIA_NIGHT_ROLES = (Roles.admin, 78361735739998228, 940911658799333408) class TriviaNightCog(commands.Cog): -- cgit v1.2.3 From 54a061841f1aafb67ecac2cee4b66dea7f72c776 Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Wed, 9 Feb 2022 21:57:59 -0500 Subject: missing extra 7 --- bot/exts/events/trivianight/trivianight.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 397ff91a..7d1f4070 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -15,7 +15,7 @@ from ._questions import QuestionView from ._scoreboard import Scoreboard # The ID you see below are the Events Lead role ID and the Event Runner Role ID -TRIVIA_NIGHT_ROLES = (Roles.admin, 78361735739998228, 940911658799333408) +TRIVIA_NIGHT_ROLES = (Roles.admin, 778361735739998228, 940911658799333408) class TriviaNightCog(commands.Cog): -- cgit v1.2.3 From 52a2bf7e373bea1e2b0738eb9d3c5561609c968c Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Sat, 12 Feb 2022 11:00:11 -0500 Subject: fix typo --- bot/exts/events/trivianight/trivianight.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 7d1f4070..18d8327a 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -15,7 +15,7 @@ from ._questions import QuestionView from ._scoreboard import Scoreboard # The ID you see below are the Events Lead role ID and the Event Runner Role ID -TRIVIA_NIGHT_ROLES = (Roles.admin, 778361735739998228, 940911658799333408) +TRIVIA_NIGHT_ROLES = (Roles.admins, 778361735739998228, 940911658799333408) class TriviaNightCog(commands.Cog): -- cgit v1.2.3 From c815a19612be9e0a28403786696964e14420204f Mon Sep 17 00:00:00 2001 From: ToxicKidz Date: Mon, 14 Feb 2022 18:26:10 -0500 Subject: fix: Add newlines in codeblock formatting --- bot/exts/utilities/githubinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index 009e0fad..963f54e5 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -257,7 +257,7 @@ class GithubInfo(commands.Cog): embed = discord.Embed( title=f"`{user_data['login']}`'s GitHub profile info", - description=f"```{user_data['bio']}```\n" if user_data["bio"] else "", + description=f"```\n{user_data['bio']}\n```\n" if user_data["bio"] else "", colour=discord.Colour.og_blurple(), url=user_data["html_url"], timestamp=datetime.strptime(user_data["created_at"], "%Y-%m-%dT%H:%M:%SZ") -- cgit v1.2.3 From 833d2e5201028e90dfb9e84da4766fba498dc04a Mon Sep 17 00:00:00 2001 From: ChrisJL Date: Tue, 15 Feb 2022 11:27:11 +0000 Subject: Disable AoC completionist task This disabled the completionist task that checks the leaderboard for people who have 50 stars and gives out the role. Since the event is running, we are not keeping the session cookies up to date, so this is flooding #dev-log with errors. This task should be altered in preparation for next event so that commenting out this line isn't required. Co-authored-by: ToxicKidz <78174417+ToxicKidz@users.noreply.github.com> --- bot/exts/events/advent_of_code/_cog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index 3acfef39..518841d4 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -61,7 +61,8 @@ class AdventOfCode(commands.Cog): self.status_task.set_name("AoC Status Countdown") self.status_task.add_done_callback(_helpers.background_task_callback) - self.completionist_task.start() + # Don't start task while event isn't running + # self.completionist_task.start() @tasks.loop(minutes=10.0) async def completionist_task(self) -> None: -- cgit v1.2.3 From f4ffffb052552ea3271b1c7d533d127d583023e4 Mon Sep 17 00:00:00 2001 From: MaskDuck Date: Wed, 16 Feb 2022 01:33:42 +0700 Subject: Fix #1024 (#1030) --- bot/exts/utilities/epoch.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/utilities/epoch.py b/bot/exts/utilities/epoch.py index 03758af0..42312dd1 100644 --- a/bot/exts/utilities/epoch.py +++ b/bot/exts/utilities/epoch.py @@ -86,7 +86,10 @@ class Epoch(commands.Cog): view = TimestampMenuView(ctx, self._format_dates(date_time), epoch) original = await ctx.send(f"`{epoch}`", view=view) await view.wait() # wait until expiration before removing the dropdown - await original.edit(view=None) + try: + await original.edit(view=None) + except discord.NotFound: # disregard the error message if the message is deleled + pass @staticmethod def _format_dates(date: arrow.Arrow) -> list[str]: -- cgit v1.2.3 From 12c4b533eef4404fc6488a39918af97e17c1f839 Mon Sep 17 00:00:00 2001 From: DMFriends <86751519+DMFriends@users.noreply.github.com> Date: Wed, 16 Feb 2022 17:14:35 -0500 Subject: Add topics for `#programming-pedagogy` channel --- bot/resources/utilities/py_topics.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'bot') diff --git a/bot/resources/utilities/py_topics.yaml b/bot/resources/utilities/py_topics.yaml index 4f684841..92fc0b73 100644 --- a/bot/resources/utilities/py_topics.yaml +++ b/bot/resources/utilities/py_topics.yaml @@ -46,6 +46,15 @@ - What modules/libraries do you want to see more projects using? - What's the most ambitious thing you've done with Python so far? +# programming-pedagogy +934931964509691966: + - What is the best way to teach/learn OOP? + - What benefits are there to teaching programming to students who aren't training to become developers? + - What are some basic concepts that we need to know before teaching programming to others? + - What are the most common difficulties/misconceptions students encounter while learning to program? + - What makes a project a good learning experience for beginners? + - What can make difficult concepts more fun for students to learn? + # algos-and-data-structs 650401909852864553: - -- cgit v1.2.3