aboutsummaryrefslogtreecommitdiffstats
path: root/bot/exts
diff options
context:
space:
mode:
Diffstat (limited to 'bot/exts')
-rw-r--r--bot/exts/evergreen/battleship.py2
-rw-r--r--bot/exts/evergreen/tic_tac_toe.py323
-rw-r--r--bot/exts/evergreen/xkcd.py89
-rw-r--r--bot/exts/halloween/spookynamerate.py401
4 files changed, 814 insertions, 1 deletions
diff --git a/bot/exts/evergreen/battleship.py b/bot/exts/evergreen/battleship.py
index 9bc374e6..fa3fb35c 100644
--- a/bot/exts/evergreen/battleship.py
+++ b/bot/exts/evergreen/battleship.py
@@ -140,7 +140,7 @@ class Game:
@staticmethod
def get_square(grid: Grid, square: str) -> Square:
"""Grabs a square from a grid with an inputted key."""
- index = ord(square[0]) - ord("A")
+ index = ord(square[0].upper()) - ord("A")
number = int(square[1:])
return grid[number-1][index] # -1 since lists are indexed from 0
diff --git a/bot/exts/evergreen/tic_tac_toe.py b/bot/exts/evergreen/tic_tac_toe.py
new file mode 100644
index 00000000..e1190502
--- /dev/null
+++ b/bot/exts/evergreen/tic_tac_toe.py
@@ -0,0 +1,323 @@
+import asyncio
+import random
+import typing as t
+
+import discord
+from discord.ext.commands import Cog, Context, check, group, guild_only
+
+from bot.bot import Bot
+from bot.constants import Emojis
+from bot.utils.pagination import LinePaginator
+
+CONFIRMATION_MESSAGE = (
+ "{opponent}, {requester} wants to play Tic-Tac-Toe against you. React to this message with "
+ f"{Emojis.confirmation} to accept or with {Emojis.decline} to decline."
+)
+
+
+def check_win(board: t.Dict[int, str]) -> bool:
+ """Check from board, is any player won game."""
+ return any(
+ (
+ # Horizontal
+ board[1] == board[2] == board[3],
+ board[4] == board[5] == board[6],
+ board[7] == board[8] == board[9],
+ # Vertical
+ board[1] == board[4] == board[7],
+ board[2] == board[5] == board[8],
+ board[3] == board[6] == board[9],
+ # Diagonal
+ board[1] == board[5] == board[9],
+ board[3] == board[5] == board[7],
+ )
+ )
+
+
+class Player:
+ """Class that contains information about player and functions that interact with player."""
+
+ def __init__(self, user: discord.User, ctx: Context, symbol: str):
+ self.user = user
+ self.ctx = ctx
+ self.symbol = symbol
+
+ async def get_move(self, board: t.Dict[int, str], msg: discord.Message) -> t.Tuple[bool, t.Optional[int]]:
+ """
+ Get move from user.
+
+ Return is timeout reached and position of field what user will fill when timeout don't reach.
+ """
+ def check_for_move(r: discord.Reaction, u: discord.User) -> bool:
+ """Check does user who reacted is user who we want, message is board and emoji is in board values."""
+ return (
+ u.id == self.user.id
+ and msg.id == r.message.id
+ and r.emoji in board.values()
+ and r.emoji in Emojis.number_emojis.values()
+ )
+
+ try:
+ react, _ = await self.ctx.bot.wait_for('reaction_add', timeout=30.0, check=check_for_move)
+ except asyncio.TimeoutError:
+ return True, None
+ else:
+ return False, list(Emojis.number_emojis.keys())[list(Emojis.number_emojis.values()).index(react.emoji)]
+
+ def __str__(self) -> str:
+ """Return mention of user."""
+ return self.user.mention
+
+
+class AI:
+ """Tic Tac Toe AI class for against computer gaming."""
+
+ def __init__(self, symbol: str):
+ self.symbol = symbol
+
+ async def get_move(self, board: t.Dict[int, str], _: discord.Message) -> t.Tuple[bool, int]:
+ """Get move from AI. AI use Minimax strategy."""
+ possible_moves = [i for i, emoji in board.items() if emoji in list(Emojis.number_emojis.values())]
+
+ for symbol in (Emojis.o, Emojis.x):
+ for move in possible_moves:
+ board_copy = board.copy()
+ board_copy[move] = symbol
+ if check_win(board_copy):
+ return False, move
+
+ open_corners = [i for i in possible_moves if i in (1, 3, 7, 9)]
+ if len(open_corners) > 0:
+ return False, random.choice(open_corners)
+
+ if 5 in possible_moves:
+ return False, 5
+
+ open_edges = [i for i in possible_moves if i in (2, 4, 6, 8)]
+ return False, random.choice(open_edges)
+
+ def __str__(self) -> str:
+ """Return `AI` as user name."""
+ return "AI"
+
+
+class Game:
+ """Class that contains information and functions about Tic Tac Toe game."""
+
+ def __init__(self, players: t.List[t.Union[Player, AI]], ctx: Context):
+ self.players = players
+ self.ctx = ctx
+ self.board = {
+ 1: Emojis.number_emojis[1],
+ 2: Emojis.number_emojis[2],
+ 3: Emojis.number_emojis[3],
+ 4: Emojis.number_emojis[4],
+ 5: Emojis.number_emojis[5],
+ 6: Emojis.number_emojis[6],
+ 7: Emojis.number_emojis[7],
+ 8: Emojis.number_emojis[8],
+ 9: Emojis.number_emojis[9]
+ }
+
+ self.current = self.players[0]
+ self.next = self.players[1]
+
+ self.winner: t.Optional[t.Union[Player, AI]] = None
+ self.loser: t.Optional[t.Union[Player, AI]] = None
+ self.over = False
+ self.canceled = False
+ self.draw = False
+
+ async def get_confirmation(self) -> t.Tuple[bool, t.Optional[str]]:
+ """
+ Ask does user want to play TicTacToe against requester. First player is always requester.
+
+ This return tuple that have:
+ - first element boolean (is game accepted?)
+ - (optional, only when first element is False, otherwise None) reason for declining.
+ """
+ confirm_message = await self.ctx.send(
+ CONFIRMATION_MESSAGE.format(
+ opponent=self.players[1].user.mention,
+ requester=self.players[0].user.mention
+ )
+ )
+ await confirm_message.add_reaction(Emojis.confirmation)
+ await confirm_message.add_reaction(Emojis.decline)
+
+ def confirm_check(reaction: discord.Reaction, user: discord.User) -> bool:
+ """Check is user who reacted from who this was requested, message is confirmation and emoji is valid."""
+ return (
+ reaction.emoji in (Emojis.confirmation, Emojis.decline)
+ and reaction.message.id == confirm_message.id
+ and user == self.players[1].user
+ )
+
+ try:
+ reaction, user = await self.ctx.bot.wait_for(
+ "reaction_add",
+ timeout=60.0,
+ check=confirm_check
+ )
+ except asyncio.TimeoutError:
+ self.over = True
+ self.canceled = True
+ await confirm_message.delete()
+ return False, "Running out of time... Cancelled game."
+
+ await confirm_message.delete()
+ if reaction.emoji == Emojis.confirmation:
+ return True, None
+ else:
+ self.over = True
+ self.canceled = True
+ return False, "User declined"
+
+ async def add_reactions(self, msg: discord.Message) -> None:
+ """Add number emojis to message."""
+ for nr in Emojis.number_emojis.values():
+ await msg.add_reaction(nr)
+
+ def format_board(self) -> str:
+ """Get formatted tic-tac-toe board for message."""
+ board = list(self.board.values())
+ return "\n".join(
+ (f"{board[line]} {board[line + 1]} {board[line + 2]}" for line in range(0, len(board), 3))
+ )
+
+ async def play(self) -> None:
+ """Start and handle game."""
+ await self.ctx.send("It's time for the game! Let's begin.")
+ board = await self.ctx.send(
+ embed=discord.Embed(description=self.format_board())
+ )
+ await self.add_reactions(board)
+
+ for _ in range(9):
+ if isinstance(self.current, Player):
+ announce = await self.ctx.send(
+ f"{self.current.user.mention}, it's your turn! "
+ "React with an emoji to take your go."
+ )
+ timeout, pos = await self.current.get_move(self.board, board)
+ if isinstance(self.current, Player):
+ await announce.delete()
+ if timeout:
+ await self.ctx.send(f"{self.current.user.mention} ran out of time. Canceling game.")
+ self.over = True
+ self.canceled = True
+ return
+ self.board[pos] = self.current.symbol
+ await board.edit(
+ embed=discord.Embed(description=self.format_board())
+ )
+ await board.clear_reaction(Emojis.number_emojis[pos])
+ if check_win(self.board):
+ self.winner = self.current
+ self.loser = self.next
+ await self.ctx.send(
+ f":tada: {self.current} won this game! :tada:"
+ )
+ await board.clear_reactions()
+ break
+ self.current, self.next = self.next, self.current
+ if not self.winner:
+ self.draw = True
+ await self.ctx.send("It's a DRAW!")
+ self.over = True
+
+
+def is_channel_free() -> t.Callable:
+ """Check is channel where command will be invoked free."""
+ async def predicate(ctx: Context) -> bool:
+ return all(game.channel != ctx.channel for game in ctx.cog.games if not game.over)
+ return check(predicate)
+
+
+def is_requester_free() -> t.Callable:
+ """Check is requester not already in any game."""
+ async def predicate(ctx: Context) -> bool:
+ return all(
+ ctx.author not in (player.user for player in game.players) for game in ctx.cog.games if not game.over
+ )
+ return check(predicate)
+
+
+class TicTacToe(Cog):
+ """TicTacToe cog contains tic-tac-toe game commands."""
+
+ def __init__(self, bot: Bot):
+ self.bot = bot
+ self.games: t.List[Game] = []
+
+ @guild_only()
+ @is_channel_free()
+ @is_requester_free()
+ @group(name="tictactoe", aliases=("ttt",), invoke_without_command=True)
+ async def tic_tac_toe(self, ctx: Context, opponent: t.Optional[discord.User]) -> None:
+ """Tic Tac Toe game. Play against friends or AI. Use reactions to add your mark to field."""
+ if opponent == ctx.author:
+ await ctx.send("You can't play against yourself.")
+ return
+ if opponent is not None and not all(
+ opponent not in (player.user for player in g.players) for g in ctx.cog.games if not g.over
+ ):
+ await ctx.send("Opponent is already in game.")
+ return
+ if opponent is None:
+ game = Game(
+ [Player(ctx.author, ctx, Emojis.x), AI(Emojis.o)],
+ ctx
+ )
+ else:
+ game = Game(
+ [Player(ctx.author, ctx, Emojis.x), Player(opponent, ctx, Emojis.o)],
+ ctx
+ )
+ self.games.append(game)
+ if opponent is not None:
+ confirmed, msg = await game.get_confirmation()
+
+ if not confirmed:
+ if msg:
+ await ctx.send(msg)
+ return
+ await game.play()
+
+ @tic_tac_toe.group(name="history", aliases=("log",), invoke_without_command=True)
+ async def tic_tac_toe_logs(self, ctx: Context) -> None:
+ """Show most recent tic-tac-toe games."""
+ if len(self.games) < 1:
+ await ctx.send("No recent games.")
+ return
+ log_games = []
+ for i, game in enumerate(self.games):
+ if game.over and not game.canceled:
+ if game.draw:
+ log_games.append(
+ f"**#{i+1}**: {game.players[0]} vs {game.players[1]} (draw)"
+ )
+ else:
+ log_games.append(
+ f"**#{i+1}**: {game.winner} :trophy: vs {game.loser}"
+ )
+ await LinePaginator.paginate(
+ log_games,
+ ctx,
+ discord.Embed(title="Most recent Tic Tac Toe games")
+ )
+
+ @tic_tac_toe_logs.command(name="show", aliases=("s",))
+ async def show_tic_tac_toe_board(self, ctx: Context, game_id: int) -> None:
+ """View game board by ID (ID is possible to get by `.tictactoe history`)."""
+ if len(self.games) < game_id:
+ await ctx.send("Game don't exist.")
+ return
+ game = self.games[game_id - 1]
+ await ctx.send(f"{game.winner} :trophy: vs {game.loser}")
+ await ctx.send(game.format_board())
+
+
+def setup(bot: Bot) -> None:
+ """Load TicTacToe Cog."""
+ bot.add_cog(TicTacToe(bot))
diff --git a/bot/exts/evergreen/xkcd.py b/bot/exts/evergreen/xkcd.py
new file mode 100644
index 00000000..d3224bfe
--- /dev/null
+++ b/bot/exts/evergreen/xkcd.py
@@ -0,0 +1,89 @@
+import logging
+import re
+from random import randint
+from typing import Dict, Optional, Union
+
+from discord import Embed
+from discord.ext import tasks
+from discord.ext.commands import Cog, Context, command
+
+from bot.bot import Bot
+from bot.constants import Colours
+
+log = logging.getLogger(__name__)
+
+COMIC_FORMAT = re.compile(r"latest|[0-9]+")
+BASE_URL = "https://xkcd.com"
+
+
+class XKCD(Cog):
+ """Retrieving XKCD comics."""
+
+ def __init__(self, bot: Bot) -> None:
+ self.bot = bot
+ self.latest_comic_info: Dict[str, Union[str, int]] = {}
+ self.get_latest_comic_info.start()
+
+ def cog_unload(self) -> None:
+ """Cancels refreshing of the task for refreshing the most recent comic info."""
+ self.get_latest_comic_info.cancel()
+
+ @tasks.loop(minutes=30)
+ async def get_latest_comic_info(self) -> None:
+ """Refreshes latest comic's information ever 30 minutes. Also used for finding a random comic."""
+ async with self.bot.http_session.get(f"{BASE_URL}/info.0.json") as resp:
+ if resp.status == 200:
+ self.latest_comic_info = await resp.json()
+ else:
+ log.debug(f"Failed to get latest XKCD comic information. Status code {resp.status}")
+
+ @command(name="xkcd")
+ async def fetch_xkcd_comics(self, ctx: Context, comic: Optional[str]) -> None:
+ """
+ Getting an xkcd comic's information along with the image.
+
+ To get a random comic, don't type any number as an argument. To get the latest, type 'latest'.
+ """
+ embed = Embed(title=f"XKCD comic '{comic}'")
+
+ embed.colour = Colours.soft_red
+
+ if comic and (comic := re.match(COMIC_FORMAT, comic)) is None:
+ embed.description = "Comic parameter should either be an integer or 'latest'."
+ await ctx.send(embed=embed)
+ return
+
+ comic = randint(1, self.latest_comic_info['num']) if comic is None else comic.group(0)
+
+ if comic == "latest":
+ info = self.latest_comic_info
+ else:
+ async with self.bot.http_session.get(f"{BASE_URL}/{comic}/info.0.json") as resp:
+ if resp.status == 200:
+ info = await resp.json()
+ else:
+ embed.title = f"XKCD comic #{comic}"
+ embed.description = f"{resp.status}: Could not retrieve xkcd comic #{comic}."
+ log.debug(f"Retrieving xkcd comic #{comic} failed with status code {resp.status}.")
+ await ctx.send(embed=embed)
+ return
+
+ embed.title = f"XKCD comic #{info['num']}"
+
+ if info["img"][-3:] in ("jpg", "png", "gif"):
+ embed.set_image(url=info["img"])
+ date = f"{info['year']}/{info['month']}/{info['day']}"
+ embed.set_footer(text=f"{date} - #{info['num']}, \'{info['safe_title']}\'")
+ embed.colour = Colours.soft_green
+ else:
+ embed.description = (
+ "The selected comic is interactive, and cannot be displayed within an embed.\n"
+ f"Comic can be viewed [here](https://xkcd.com/{info['num']})."
+ )
+
+ await ctx.send(embed=embed)
+
+
+def setup(bot: Bot) -> None:
+ """Loading the XKCD cog."""
+ bot.add_cog(XKCD(bot))
diff --git a/bot/exts/halloween/spookynamerate.py b/bot/exts/halloween/spookynamerate.py
new file mode 100644
index 00000000..e2950343
--- /dev/null
+++ b/bot/exts/halloween/spookynamerate.py
@@ -0,0 +1,401 @@
+import asyncio
+import json
+import random
+from collections import defaultdict
+from datetime import datetime, timedelta
+from logging import getLogger
+from os import getenv
+from pathlib import Path
+from typing import Dict, Union
+
+from async_rediscache import RedisCache
+from discord import Embed, Reaction, TextChannel, User
+from discord.colour import Colour
+from discord.ext import tasks
+from discord.ext.commands import Bot, Cog, Context, group
+
+from bot.constants import Channels, Client, Colours, Month
+from bot.utils.decorators import InMonthCheckFailure
+
+logger = getLogger(__name__)
+
+EMOJIS_VAL = {
+ "\N{Jack-O-Lantern}": 1,
+ "\N{Ghost}": 2,
+ "\N{Skull and Crossbones}": 3,
+ "\N{Zombie}": 4,
+ "\N{Face Screaming In Fear}": 5,
+}
+ADDED_MESSAGES = [
+ "Let's see if you win?",
+ ":jack_o_lantern: SPOOKY :jack_o_lantern:",
+ "If you got it, haunt it.",
+ "TIME TO GET YOUR SPOOKY ON! :skull:",
+]
+PING = "<@{id}>"
+
+EMOJI_MESSAGE = "\n".join([f"- {emoji} {val}" for emoji, val in EMOJIS_VAL.items()])
+HELP_MESSAGE_DICT = {
+ "title": "Spooky Name Rate",
+ "description": f"Help for the `{Client.prefix}spookynamerate` command",
+ "color": Colours.soft_orange,
+ "fields": [
+ {
+ "name": "How to play",
+ "value": (
+ "Everyday, the bot will post a random name, which you will need to spookify using your creativity.\n"
+ "You can rate each message according to how scary it is.\n"
+ "At the end of the day, the author of the message with most reactions will be the winner of the day.\n"
+ f"On a scale of 1 to {len(EMOJIS_VAL)}, the reactions order:\n"
+ f"{EMOJI_MESSAGE}"
+ ),
+ "inline": False,
+ },
+ {
+ "name": "How do I add my spookified name?",
+ "value": f"Simply type `{Client.prefix}spookynamerate add my name`",
+ "inline": False,
+ },
+ {
+ "name": "How do I *delete* my spookified name?",
+ "value": f"Simply type `{Client.prefix}spookynamerate delete`",
+ "inline": False,
+ },
+ ],
+}
+
+
+class SpookyNameRate(Cog):
+ """
+ A game that asks the user to spookify or halloweenify a name that is given everyday.
+
+ It sends a random name everyday. The user needs to try and spookify it to his best ability and
+ send that name back using the `spookynamerate add entry` command
+ """
+
+ # This cache stores the message id of each added word along with a dictionary which contains the name the author
+ # added, the author's id, and the author's score (which is 0 by default)
+ messages = RedisCache()
+
+ # The data cache stores small information such as the current name that is going on and whether it is the first time
+ # the bot is running
+ data = RedisCache()
+ debug = getenv('SPOOKYNAMERATE_DEBUG', False) # Enable if you do not want to limit the commands to October or if
+ # you do not want to wait till 12 UTC. Note: if debug is enabled and you run `.cogs reload spookynamerate`, it
+ # will automatically start the scoring and announcing the result (without waiting for 12, so do not expect it to.).
+ # Also, it won't wait for the two hours (when the poll closes).
+
+ def __init__(self, bot: Bot) -> None:
+ self.bot = bot
+
+ names_data = self.load_json(
+ Path("bot", "resources", "halloween", "spookynamerate_names.json")
+ )
+ self.first_names = names_data["first_names"]
+ self.last_names = names_data["last_names"]
+ # the names are from https://www.mockaroo.com/
+
+ self.name = None
+
+ self.bot.loop.create_task(self.load_vars())
+
+ self.first_time = None
+ self.poll = False
+ self.announce_name.start()
+ self.checking_messages = asyncio.Lock()
+ # Define an asyncio.Lock() to make sure the dictionary isn't changed
+ # when checking the messages for duplicate emojis'
+
+ async def load_vars(self) -> None:
+ """Loads the variables that couldn't be loaded in __init__."""
+ self.first_time = await self.data.get("first_time", True)
+ self.name = await self.data.get("name")
+
+ @group(name="spookynamerate", invoke_without_command=True)
+ async def spooky_name_rate(self, ctx: Context) -> None:
+ """Get help on the Spooky Name Rate game."""
+ await ctx.send(embed=Embed.from_dict(HELP_MESSAGE_DICT))
+
+ @spooky_name_rate.command(name="list", aliases=["all", "entries"])
+ async def list_entries(self, ctx: Context) -> None:
+ """Send all the entries up till now in a single embed."""
+ await ctx.send(embed=await self.get_responses_list(final=False))
+
+ @spooky_name_rate.command(name="name")
+ async def tell_name(self, ctx: Context) -> None:
+ """Tell the current random name."""
+ if not self.poll:
+ await ctx.send(f"The name is **{self.name}**")
+ return
+
+ await ctx.send(
+ f"The name ~~is~~ was **{self.name}**. The poll has already started, so you cannot "
+ "add an entry."
+ )
+
+ @spooky_name_rate.command(name="add", aliases=["register"])
+ async def add_name(self, ctx: Context, *, name: str) -> None:
+ """Use this command to add/register your spookified name."""
+ if self.poll:
+ logger.info(f"{ctx.message.author} tried to add a name, but the poll had already started.")
+ await ctx.send("Sorry, the poll has started! You can try and participate in the next round though!")
+ return
+
+ message = ctx.message
+
+ for data in (json.loads(user_data) for _, user_data in await self.messages.items()):
+ if data["author"] == message.author.id:
+ await ctx.send(
+ "But you have already added an entry! Type "
+ f"`{self.bot.command_prefix}spookynamerate "
+ "delete` to delete it, and then you can add it again"
+ )
+ return
+
+ elif data["name"] == name:
+ await ctx.send("TOO LATE. Someone has already added this name.")
+ return
+
+ msg = await (await self.get_channel()).send(f"{message.author.mention} added the name {name!r}!")
+
+ await self.messages.set(
+ msg.id,
+ json.dumps(
+ {
+ "name": name,
+ "author": message.author.id,
+ "score": 0,
+ }
+ ),
+ )
+
+ for emoji in EMOJIS_VAL:
+ await msg.add_reaction(emoji)
+
+ logger.info(f"{message.author} added the name {name!r}")
+
+ @spooky_name_rate.command(name="delete")
+ async def delete_name(self, ctx: Context) -> None:
+ """Delete the user's name."""
+ if self.poll:
+ await ctx.send("You can't delete your name since the poll has already started!")
+ return
+ for message_id, data in await self.messages.items():
+ data = json.loads(data)
+
+ if ctx.author.id == data["author"]:
+ await self.messages.delete(message_id)
+ await ctx.send(f'Name deleted successfully ({data["name"]!r})!')
+ return
+
+ await ctx.send(
+ f"But you don't have an entry... :eyes: Type `{self.bot.command_prefix}spookynamerate add your entry`"
+ )
+
+ @Cog.listener()
+ async def on_reaction_add(self, reaction: Reaction, user: User) -> None:
+ """Ensures that each user adds maximum one reaction."""
+ if user.bot or not await self.messages.contains(reaction.message.id):
+ return
+
+ async with self.checking_messages: # Acquire the lock so that the dictionary isn't reset while iterating.
+ if reaction.emoji in EMOJIS_VAL:
+ # create a custom counter
+ reaction_counter = defaultdict(int)
+ for msg_reaction in reaction.message.reactions:
+ async for reaction_user in msg_reaction.users():
+ if reaction_user == self.bot.user:
+ continue
+ reaction_counter[reaction_user] += 1
+
+ if reaction_counter[user] > 1:
+ await user.send(
+ "Sorry, you have already added a reaction, "
+ "please remove your reaction and try again."
+ )
+ await reaction.remove(user)
+ return
+
+ @tasks.loop(hours=24.0)
+ async def announce_name(self) -> None:
+ """Announces the name needed to spookify every 24 hours and the winner of the previous game."""
+ if not self.in_allowed_month():
+ return
+
+ channel = await self.get_channel()
+
+ if self.first_time:
+ await channel.send(
+ "Okkey... Welcome to the **Spooky Name Rate Game**! It's a relatively simple game.\n"
+ f"Everyday, a random name will be sent in <#{Channels.community_bot_commands}> "
+ "and you need to try and spookify it!\nRegister your name using "
+ f"`{self.bot.command_prefix}spookynamerate add spookified name`"
+ )
+
+ await self.data.set("first_time", False)
+ self.first_time = False
+
+ else:
+ if await self.messages.items():
+ await channel.send(embed=await self.get_responses_list(final=True))
+ self.poll = True
+ if not SpookyNameRate.debug:
+ await asyncio.sleep(2 * 60 * 60) # sleep for two hours
+
+ logger.info("Calculating score")
+ for message_id, data in await self.messages.items():
+ data = json.loads(data)
+
+ msg = await channel.fetch_message(message_id)
+ score = 0
+ for reaction in msg.reactions:
+ reaction_value = EMOJIS_VAL.get(reaction.emoji, 0) # get the value of the emoji else 0
+ score += reaction_value * (reaction.count - 1) # multiply by the num of reactions
+ # subtract one, since one reaction was done by the bot
+
+ logger.debug(f"{self.bot.get_user(data['author'])} got a score of {score}")
+ data["score"] = score
+ await self.messages.set(message_id, json.dumps(data))
+
+ # Sort the winner messages
+ winner_messages = sorted(
+ ((msg_id, json.loads(usr_data)) for msg_id, usr_data in await self.messages.items()),
+ key=lambda x: x[1]["score"],
+ reverse=True,
+ )
+
+ winners = []
+ for i, winner in enumerate(winner_messages):
+ winners.append(winner)
+ if len(winner_messages) > i + 1:
+ if winner_messages[i + 1][1]["score"] != winner[1]["score"]:
+ break
+ elif len(winner_messages) == (i + 1) + 1: # The next element is the last element
+ if winner_messages[i + 1][1]["score"] != winner[1]["score"]:
+ break
+
+ # one iteration is complete
+ await channel.send("Today's Spooky Name Rate Game ends now, and the winner(s) is(are)...")
+
+ async with channel.typing():
+ await asyncio.sleep(1) # give the drum roll feel
+
+ if not winners: # There are no winners (no participants)
+ await channel.send("Hmm... Looks like no one participated! :cry:")
+ return
+
+ score = winners[0][1]["score"]
+ congratulations = "to all" if len(winners) > 1 else PING.format(id=winners[0][1]["author"])
+ names = ", ".join(f'{win[1]["name"]} ({PING.format(id=win[1]["author"])})' for win in winners)
+
+ # display winners, their names and scores
+ await channel.send(
+ f"Congratulations {congratulations}!\n"
+ f"You have a score of {score}!\n"
+ f"Your name{ 's were' if len(winners) > 1 else 'was'}:\n{names}"
+ )
+
+ # Send random party emojis
+ party = (random.choice([":partying_face:", ":tada:"]) for _ in range(random.randint(1, 10)))
+ await channel.send(" ".join(party))
+
+ async with self.checking_messages: # Acquire the lock to delete the messages
+ await self.messages.clear() # reset the messages
+
+ # send the next name
+ self.name = f"{random.choice(self.first_names)} {random.choice(self.last_names)}"
+ await self.data.set("name", self.name)
+
+ await channel.send(
+ "Let's move on to the next name!\nAnd the next name is...\n"
+ f"**{self.name}**!\nTry to spookify that... :smirk:"
+ )
+
+ self.poll = False # accepting responses
+
+ @announce_name.before_loop
+ async def wait_till_scheduled_time(self) -> None:
+ """Waits till the next day's 12PM if crossed it, otherwise waits till the same day's 12PM."""
+ if SpookyNameRate.debug:
+ return
+
+ now = datetime.utcnow()
+ if now.hour < 12:
+ twelve_pm = now.replace(hour=12, minute=0, second=0, microsecond=0)
+ time_left = twelve_pm - now
+ await asyncio.sleep(time_left.seconds)
+ return
+
+ tomorrow_12pm = now + timedelta(days=1)
+ tomorrow_12pm = tomorrow_12pm.replace(hour=12, minute=0, second=0, microsecond=0)
+ await asyncio.sleep((tomorrow_12pm - now).seconds)
+
+ async def get_responses_list(self, final: bool = False) -> Embed:
+ """Returns an embed containing the responses of the people."""
+ channel = await self.get_channel()
+
+ embed = Embed(color=Colour.red())
+
+ if await self.messages.items():
+ if final:
+ embed.title = "Spooky Name Rate is about to end!"
+ embed.description = (
+ "This Spooky Name Rate round is about to end in 2 hours! You can review "
+ "the entries below! Have you rated other's names?"
+ )
+ else:
+ embed.title = "All the spookified names!"
+ embed.description = "See a list of all the entries entered by everyone!"
+ else:
+ embed.title = "No one has added an entry yet..."
+
+ for message_id, data in await self.messages.items():
+ data = json.loads(data)
+
+ embed.add_field(
+ name=(self.bot.get_user(data["author"]) or await self.bot.fetch_user(data["author"])).name,
+ value=f"[{(data)['name']}](https://discord.com/channels/{Client.guild}/{channel.id}/{message_id})",
+ )
+
+ return embed
+
+ async def get_channel(self) -> Union[TextChannel, None]:
+ """Gets the sir-lancebot-channel after waiting until ready."""
+ await self.bot.wait_until_ready()
+ channel = self.bot.get_channel(
+ Channels.community_bot_commands
+ ) or await self.bot.fetch_channel(Channels.community_bot_commands)
+ if not channel:
+ logger.warning("Bot is unable to get the #seasonalbot-commands channel. Please check the channel ID.")
+ return channel
+
+ @staticmethod
+ def load_json(file: Path) -> Dict[str, str]:
+ """Loads a JSON file and returns its contents."""
+ with file.open("r", encoding="utf-8") as f:
+ return json.load(f)
+
+ @staticmethod
+ def in_allowed_month() -> bool:
+ """Returns whether running in the limited month."""
+ if SpookyNameRate.debug:
+ return True
+
+ if not Client.month_override:
+ return datetime.utcnow().month == Month.OCTOBER
+ return Client.month_override == Month.OCTOBER
+
+ def cog_check(self, ctx: Context) -> bool:
+ """A command to check whether the command is being called in October."""
+ if not self.in_allowed_month():
+ raise InMonthCheckFailure("You can only use these commands in October!")
+ return True
+
+ def cog_unload(self) -> None:
+ """Stops the announce_name task."""
+ self.announce_name.cancel()
+
+
+def setup(bot: Bot) -> None:
+ """Loads the SpookyNameRate Cog."""
+ bot.add_cog(SpookyNameRate(bot))