🔀 change reactions into buttons
Add components
This commit is contained in:
@ -30,9 +30,9 @@ class EventCog(commands.Cog):
|
||||
await self.bot.error_handler.on_error(method)
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_reaction_add(self, reaction, user):
|
||||
async def on_component(self, ctx):
|
||||
"""Handle when someone reacts to a message."""
|
||||
await self.bot.event_handler.on_reaction_add(reaction, user)
|
||||
await self.bot.event_handler.on_component(ctx)
|
||||
|
||||
|
||||
def setup(bot):
|
||||
|
@ -105,11 +105,6 @@ class ConnectFourCog(commands.Cog):
|
||||
"""Start a game of connect four against Gwendolyn."""
|
||||
await self.bot.games.connect_four.start(ctx, difficulty)
|
||||
|
||||
@cog_ext.cog_subcommand(**params["connect_four_surrender"])
|
||||
async def connect_four_surrender(self, ctx):
|
||||
"""Surrender the game of connect four."""
|
||||
await self.bot.games.connect_four.surrender(ctx)
|
||||
|
||||
|
||||
class HangmanCog(commands.Cog):
|
||||
"""Contains all the hangman commands."""
|
||||
@ -118,16 +113,11 @@ class HangmanCog(commands.Cog):
|
||||
"""Initialize the cog."""
|
||||
self.bot = bot
|
||||
|
||||
@cog_ext.cog_subcommand(**params["hangman_start"])
|
||||
async def hangman_start(self, ctx):
|
||||
@cog_ext.cog_slash(**params["hangman"])
|
||||
async def hangman(self, ctx):
|
||||
"""Start a game of hangman."""
|
||||
await self.bot.games.hangman.start(ctx)
|
||||
|
||||
@cog_ext.cog_subcommand(**params["hangman_stop"])
|
||||
async def hangman_stop(self, ctx):
|
||||
"""Stop the current game of hangman."""
|
||||
await self.bot.games.hangman.stop(ctx)
|
||||
|
||||
|
||||
class HexCog(commands.Cog):
|
||||
"""Contains all the hex commands."""
|
||||
|
@ -15,7 +15,13 @@ import discord # Used for typehints, discord.file and to check whether
|
||||
# the opponent in ConnectFour.start is a discord.User
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont # Used by DrawConnectFour()
|
||||
from discord_slash.context import SlashContext # Used for typehints
|
||||
from discord_slash.context import SlashContext, ComponentContext # Used for
|
||||
# typehints
|
||||
from discord_slash.utils.manage_components import (create_button,
|
||||
create_actionrow)
|
||||
from discord_slash.model import ButtonStyle
|
||||
|
||||
from gwendolyn.utils import encode_id
|
||||
|
||||
ROWCOUNT = 6
|
||||
COLUMNCOUNT = 7
|
||||
@ -37,7 +43,7 @@ class ConnectFour():
|
||||
"""Initialize the class."""
|
||||
self.bot = bot
|
||||
self.draw = DrawConnectFour(bot)
|
||||
self.database_funcs = self.bot.database_funcs
|
||||
self.get_name = self.bot.database_funcs.get_name
|
||||
# pylint: disable=invalid-name
|
||||
self.AISCORES = {
|
||||
"middle": 3,
|
||||
@ -49,9 +55,41 @@ class ConnectFour():
|
||||
"win": 10000,
|
||||
"avoid losing": 100
|
||||
}
|
||||
self.REACTIONS = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣"]
|
||||
# pylint: enable=invalid-name
|
||||
|
||||
def _encode_board_string(self, board: list):
|
||||
string = [str(i) for row in board for i in row]
|
||||
|
||||
while len(string) > 0 and string[0] == "0":
|
||||
string = string[1:]
|
||||
|
||||
if string == "":
|
||||
string = "0"
|
||||
|
||||
dec = 0
|
||||
for i, digit in enumerate(string[::-1]):
|
||||
dec += (3**i)*int(digit)
|
||||
|
||||
return str(dec)
|
||||
|
||||
def _decode_board_string(self, board_string: str):
|
||||
dec = int(board_string)
|
||||
string = []
|
||||
while dec:
|
||||
string.append(str(dec % 3))
|
||||
dec = dec // 3
|
||||
|
||||
while len(string) < ROWCOUNT * COLUMNCOUNT:
|
||||
string.append("0")
|
||||
|
||||
string = string[::-1]
|
||||
|
||||
board = [
|
||||
[int(x) for x in string[i*COLUMNCOUNT:i*COLUMNCOUNT+COLUMNCOUNT]]
|
||||
for i in range(ROWCOUNT)]
|
||||
|
||||
return board
|
||||
|
||||
async def start(self, ctx: SlashContext,
|
||||
opponent: Union[int, discord.User]):
|
||||
"""
|
||||
@ -68,23 +106,18 @@ class ConnectFour():
|
||||
searches when minimaxing.
|
||||
"""
|
||||
await self.bot.defer(ctx)
|
||||
user = f"#{ctx.author.id}"
|
||||
user = ctx.author.id
|
||||
channel = str(ctx.channel_id)
|
||||
game = self.bot.database["connect 4 games"].find_one({"_id": channel})
|
||||
|
||||
started_game = False
|
||||
can_start = True
|
||||
|
||||
if game is not None:
|
||||
send_message = self.bot.long_strings["Connect 4 going on"]
|
||||
log_message = "There was already a game going on"
|
||||
can_start = False
|
||||
elif isinstance(opponent, int):
|
||||
if isinstance(opponent, int):
|
||||
# Opponent is Gwendolyn
|
||||
if opponent in range(1, 6):
|
||||
difficulty = int(opponent)
|
||||
difficulty_text = f" with difficulty {difficulty}"
|
||||
opponent = f"#{self.bot.user.id}"
|
||||
opponent = self.bot.user.id
|
||||
else:
|
||||
send_message = "Difficulty doesn't exist"
|
||||
log_message = "They challenged a difficulty that doesn't exist"
|
||||
@ -96,7 +129,7 @@ class ConnectFour():
|
||||
# It was Gwendolyn
|
||||
difficulty = 3
|
||||
difficulty_text = f" with difficulty {difficulty}"
|
||||
opponent = f"#{self.bot.user.id}"
|
||||
opponent = self.bot.user.id
|
||||
else:
|
||||
send_message = "You can't challenge a bot!"
|
||||
log_message = "They tried to challenge a bot"
|
||||
@ -104,7 +137,7 @@ class ConnectFour():
|
||||
else:
|
||||
# Opponent is another player
|
||||
if ctx.author != opponent:
|
||||
opponent = f"#{opponent.id}"
|
||||
opponent = opponent.id
|
||||
difficulty = 5
|
||||
difficulty_text = ""
|
||||
else:
|
||||
@ -117,26 +150,13 @@ class ConnectFour():
|
||||
players = [user, opponent]
|
||||
random.shuffle(players)
|
||||
|
||||
new_game = {
|
||||
"_id": channel,
|
||||
"board": board,
|
||||
"winner": 0,
|
||||
"win direction": "",
|
||||
"win coordinates": [0, 0],
|
||||
"players": players,
|
||||
"turn": 0,
|
||||
"difficulty": difficulty
|
||||
}
|
||||
self.draw.draw_image(channel, board, 0, [0,0], "", players)
|
||||
|
||||
self.bot.database["connect 4 games"].insert_one(new_game)
|
||||
|
||||
self.draw.draw_image(channel)
|
||||
|
||||
gwendolyn_turn = (players[0] == f"#{self.bot.user.id}")
|
||||
gwendolyn_turn = (players[0] == self.bot.user.id)
|
||||
started_game = True
|
||||
|
||||
opponent_name = self.database_funcs.get_name(opponent)
|
||||
turn_name = self.database_funcs.get_name(players[0])
|
||||
opponent_name = self.get_name(f"#{opponent}")
|
||||
turn_name = self.get_name(f"#{players[0]}")
|
||||
|
||||
started_text = "Started game against {}{}.".format(
|
||||
opponent_name,
|
||||
@ -147,138 +167,202 @@ class ConnectFour():
|
||||
log_message = "They started a game"
|
||||
|
||||
self.bot.log(log_message)
|
||||
await ctx.send(send_message)
|
||||
|
||||
# Sets the whole game in motion
|
||||
if started_game:
|
||||
boards_path = "gwendolyn/resources/games/connect_four_boards/"
|
||||
file_path = f"{boards_path}board{ctx.channel_id}.png"
|
||||
old_image = await ctx.channel.send(file=discord.File(file_path))
|
||||
|
||||
old_images_path = "gwendolyn/resources/games/old_images/"
|
||||
old_image_path = f"{old_images_path}connect_four{ctx.channel_id}"
|
||||
with open(old_image_path, "w") as file_pointer:
|
||||
file_pointer.write(str(old_image.id))
|
||||
image_message = await ctx.send(
|
||||
send_message, file=discord.File(file_path)
|
||||
)
|
||||
|
||||
board_string = self._encode_board_string(board)
|
||||
if gwendolyn_turn:
|
||||
await self._connect_four_ai(ctx)
|
||||
await self._connect_four_ai(
|
||||
ctx, board_string, players, difficulty, image_message.id
|
||||
)
|
||||
else:
|
||||
for reaction in self.REACTIONS:
|
||||
await old_image.add_reaction(reaction)
|
||||
buttons = []
|
||||
for i in range(7):
|
||||
custom_id = encode_id(
|
||||
[
|
||||
"connectfour",
|
||||
"place",
|
||||
str(players[0]),
|
||||
board_string,
|
||||
str(i),
|
||||
str(players[0]),
|
||||
str(players[1]),
|
||||
str(difficulty),
|
||||
str(image_message.id)
|
||||
]
|
||||
)
|
||||
buttons.append(create_button(
|
||||
style=ButtonStyle.blue,
|
||||
label=str(i+1),
|
||||
custom_id=custom_id,
|
||||
disabled=(board[0][i] != 0)
|
||||
))
|
||||
|
||||
async def place_piece(self, ctx: Union[SlashContext, discord.Message],
|
||||
user: int, column: int):
|
||||
custom_id = encode_id(
|
||||
[
|
||||
"connectfour",
|
||||
"end",
|
||||
str(players[0]),
|
||||
str(players[1]),
|
||||
str(image_message.id)
|
||||
]
|
||||
)
|
||||
buttons.append(create_button(
|
||||
style=ButtonStyle.red,
|
||||
label="Surrender",
|
||||
custom_id=custom_id
|
||||
))
|
||||
|
||||
action_rows = []
|
||||
for x in range(((len(buttons)-1)//4)+1):
|
||||
row_buttons = buttons[
|
||||
(x*4):(min(len(buttons),x*4+4))
|
||||
]
|
||||
action_rows.append(create_actionrow(*row_buttons))
|
||||
|
||||
await image_message.edit(
|
||||
components=action_rows
|
||||
)
|
||||
else:
|
||||
await ctx.send(send_message)
|
||||
|
||||
async def place_piece(self, ctx: ComponentContext, board_string: str,
|
||||
column: int, players: list[int], difficulty: int,
|
||||
placer: int, message_id: int):
|
||||
"""
|
||||
Place a piece on the board.
|
||||
|
||||
*Parameters*
|
||||
------------
|
||||
ctx: Union[SlashContext, discord.Message]
|
||||
The context of the command/reaction. ctx can only be
|
||||
SlashContext if the piece is placed by the bot
|
||||
immediately after the player starts the game with a
|
||||
command. Otherwise, ctx will be the message the player
|
||||
reacted to when placing a piece.
|
||||
user: int
|
||||
The player-number of the player placing the piece.
|
||||
column: int
|
||||
The column the player is placing the piece in.
|
||||
"""
|
||||
channel = str(ctx.channel.id)
|
||||
connect_four_games = self.bot.database["connect 4 games"]
|
||||
game = connect_four_games.find_one({"_id": channel})
|
||||
player_number = game["players"].index(user)+1
|
||||
user_name = self.database_funcs.get_name(user)
|
||||
|
||||
player_number = players.index(placer)+1
|
||||
user_name = self.get_name(f"#{placer}")
|
||||
placed_piece = False
|
||||
|
||||
if game is None:
|
||||
send_message = "There's no game in this channel"
|
||||
log_message = "There was no game in the channel"
|
||||
board = self._decode_board_string(board_string)
|
||||
board = self._place_on_board(board, player_number, column)
|
||||
|
||||
if board is None:
|
||||
send_message = "There isn't any room in that column"
|
||||
log_message = "There wasn't any room in the column"
|
||||
else:
|
||||
board = game["board"]
|
||||
board = self._place_on_board(board, player_number, column)
|
||||
turn = player_number % 2
|
||||
|
||||
if board is None:
|
||||
send_message = "There isn't any room in that column"
|
||||
log_message = "There wasn't any room in the column"
|
||||
self.bot.log("Checking for win")
|
||||
winner, win_direction, win_coordinates = self._is_won(board)
|
||||
|
||||
if winner != 0:
|
||||
game_won = True
|
||||
send_message = "{} placed a piece in column {} and won. "
|
||||
send_message = send_message.format(user_name, column+1)
|
||||
log_message = f"{user_name} won"
|
||||
win_amount = difficulty**2+5
|
||||
if players[winner-1] != self.bot.user.id:
|
||||
send_message += "Adding {} GwendoBucks to their account"
|
||||
send_message = send_message.format(win_amount)
|
||||
elif 0 not in board[0]:
|
||||
game_won = True
|
||||
send_message = "It's a draw!"
|
||||
log_message = "The game ended in a draw"
|
||||
else:
|
||||
updater = {"$set": {"board": board}}
|
||||
connect_four_games.update_one({"_id": channel}, updater)
|
||||
turn = (game["turn"]+1) % 2
|
||||
updater = {"$set": {"turn": turn}}
|
||||
connect_four_games.update_one({"_id": channel}, updater)
|
||||
game_won = False
|
||||
other_user_id = f"#{players[turn]}"
|
||||
other_user_name = self.get_name(other_user_id)
|
||||
send_message = self.bot.long_strings["Connect 4 placed"]
|
||||
format_parameters = [user_name, column+1, other_user_name]
|
||||
send_message = send_message.format(*format_parameters)
|
||||
log_message = "They placed the piece"
|
||||
|
||||
self.bot.log("Checking for win")
|
||||
won, win_direction, win_coordinates = self._is_won(board)
|
||||
gwendolyn_turn = (players[turn] == self.bot.user.id)
|
||||
|
||||
if won != 0:
|
||||
game_won = True
|
||||
updater = {"$set": {"winner": won}}
|
||||
connect_four_games.update_one({"_id": channel}, updater)
|
||||
updater = {"$set": {"win direction": win_direction}}
|
||||
connect_four_games.update_one({"_id": channel}, updater)
|
||||
updater = {"$set": {"win coordinates": win_coordinates}}
|
||||
connect_four_games.update_one({"_id": channel}, updater)
|
||||
placed_piece = True
|
||||
|
||||
send_message = "{} placed a piece in column {} and won. "
|
||||
send_message = send_message.format(user_name, column+1)
|
||||
log_message = f"{user_name} won"
|
||||
win_amount = int(game["difficulty"])**2+5
|
||||
if game["players"][won-1] != f"#{self.bot.user.id}":
|
||||
send_message += "Adding {} GwendoBucks to their account"
|
||||
send_message = send_message.format(win_amount)
|
||||
elif 0 not in board[0]:
|
||||
game_won = True
|
||||
send_message = "It's a draw!"
|
||||
log_message = "The game ended in a draw"
|
||||
else:
|
||||
game_won = False
|
||||
other_user_id = game["players"][turn]
|
||||
other_user_name = self.database_funcs.get_name(
|
||||
other_user_id)
|
||||
send_message = self.bot.long_strings["Connect 4 placed"]
|
||||
format_parameters = [user_name, column+1, other_user_name]
|
||||
send_message = send_message.format(*format_parameters)
|
||||
log_message = "They placed the piece"
|
||||
|
||||
gwendolyn_turn = (
|
||||
game["players"][turn] == f"#{self.bot.user.id}")
|
||||
|
||||
placed_piece = True
|
||||
|
||||
await ctx.channel.send(send_message)
|
||||
self.bot.log(log_message)
|
||||
|
||||
if placed_piece:
|
||||
self.draw.draw_image(channel)
|
||||
channel = str(ctx.channel)
|
||||
self.draw.draw_image(
|
||||
channel, board, winner, win_coordinates, win_direction, players)
|
||||
|
||||
old_images_path = "gwendolyn/resources/games/old_images/"
|
||||
old_image_path = old_images_path + f"connect_four{channel}"
|
||||
with open(old_image_path, "r") as file_pointer:
|
||||
old_image_id = int(file_pointer.read())
|
||||
old_image = await ctx.channel.fetch_message(old_image_id)
|
||||
|
||||
if old_image is not None:
|
||||
await old_image.delete()
|
||||
else:
|
||||
self.bot.log("The old image was already deleted")
|
||||
|
||||
boards_path = "gwendolyn/resources/games/connect_four_boards/"
|
||||
file_path = boards_path + f"board{channel}.png"
|
||||
old_image = await ctx.channel.send(file=discord.File(file_path))
|
||||
image_message = await ctx.channel.send(
|
||||
send_message, file=discord.File(file_path))
|
||||
|
||||
if game_won:
|
||||
self._end_game(channel)
|
||||
self._end_game(winner, players, difficulty)
|
||||
else:
|
||||
with open(old_image_path, "w") as file_pointer:
|
||||
file_pointer.write(str(old_image.id))
|
||||
board_string = self._encode_board_string(board)
|
||||
if gwendolyn_turn:
|
||||
await self._connect_four_ai(ctx)
|
||||
await self._connect_four_ai(
|
||||
ctx, board_string, players, difficulty, image_message.id
|
||||
)
|
||||
else:
|
||||
for reaction in self.REACTIONS:
|
||||
await old_image.add_reaction(reaction)
|
||||
buttons = []
|
||||
for i in range(7):
|
||||
custom_id = encode_id(
|
||||
[
|
||||
"connectfour",
|
||||
"place",
|
||||
str(players[turn]),
|
||||
board_string,
|
||||
str(i),
|
||||
str(players[0]),
|
||||
str(players[1]),
|
||||
str(difficulty),
|
||||
str(image_message.id)
|
||||
]
|
||||
)
|
||||
buttons.append(create_button(
|
||||
style=ButtonStyle.blue,
|
||||
label=str(i+1),
|
||||
custom_id=custom_id,
|
||||
disabled=(board[0][i] != 0)
|
||||
))
|
||||
|
||||
async def surrender(self, ctx: SlashContext):
|
||||
custom_id = encode_id(
|
||||
[
|
||||
"connectfour",
|
||||
"end",
|
||||
str(players[0]),
|
||||
str(players[1]),
|
||||
str(difficulty),
|
||||
str(image_message.id)
|
||||
]
|
||||
)
|
||||
buttons.append(create_button(
|
||||
style=ButtonStyle.red,
|
||||
label="Surrender",
|
||||
custom_id=custom_id
|
||||
))
|
||||
|
||||
action_rows = []
|
||||
for x in range(((len(buttons)-1)//4)+1):
|
||||
row_buttons = buttons[
|
||||
(x*4):(min(len(buttons),x*4+4))
|
||||
]
|
||||
action_rows.append(create_actionrow(*row_buttons))
|
||||
|
||||
await image_message.edit(
|
||||
components=action_rows
|
||||
)
|
||||
else:
|
||||
await ctx.channel.send(send_message)
|
||||
|
||||
old_message = await ctx.channel.fetch_message(message_id)
|
||||
await old_message.delete()
|
||||
|
||||
async def surrender(self, ctx: ComponentContext, players: list,
|
||||
difficulty: int, message_id: int):
|
||||
"""
|
||||
Surrender a connect four game.
|
||||
|
||||
@ -287,38 +371,25 @@ class ConnectFour():
|
||||
ctx: SlashContext
|
||||
The context of the command.
|
||||
"""
|
||||
await self.bot.defer(ctx)
|
||||
channel = str(ctx.channel_id)
|
||||
game = self.bot.database["connect 4 games"].find_one({"_id": channel})
|
||||
|
||||
if f"#{ctx.author.id}" in game["players"]:
|
||||
loser_index = game["players"].index(f"#{ctx.author.id}")
|
||||
winner_index = (loser_index+1) % 2
|
||||
winner_id = game["players"][winner_index]
|
||||
winner_name = self.database_funcs.get_name(winner_id)
|
||||
loser_index = players.index(ctx.author.id)
|
||||
winner_index = (loser_index+1) % 2
|
||||
winner_id = players[winner_index]
|
||||
winner_name = self.get_name(f"#{winner_id}")
|
||||
|
||||
send_message = f"{ctx.author.display_name} surrenders."
|
||||
send_message += f" This means {winner_name} is the winner."
|
||||
if winner_id != f"#{self.bot.user.id}":
|
||||
difficulty = int(game["difficulty"])
|
||||
reward = difficulty**2 + 5
|
||||
send_message += f" Adding {reward} to their account"
|
||||
send_message = f"{ctx.author.display_name} surrenders."
|
||||
send_message += f" This means {winner_name} is the winner."
|
||||
if winner_id != self.bot.user.id:
|
||||
difficulty = int(difficulty)
|
||||
reward = difficulty**2 + 5
|
||||
send_message += f" Adding {reward} to their account"
|
||||
|
||||
await ctx.send(send_message)
|
||||
old_images_path = "gwendolyn/resources/games/old_images/"
|
||||
old_image_path = old_images_path + f"connect_four{channel}"
|
||||
with open(old_image_path, "r") as file_pointer:
|
||||
old_image_id = int(file_pointer.read())
|
||||
old_image = await ctx.channel.fetch_message(old_image_id)
|
||||
await ctx.channel.send(send_message)
|
||||
|
||||
if old_image is not None:
|
||||
await old_image.delete()
|
||||
else:
|
||||
self.bot.log("The old image was already deleted")
|
||||
self._end_game(winner_index+1, players, difficulty)
|
||||
old_image = await ctx.channel.fetch_message(message_id)
|
||||
await old_image.delete()
|
||||
|
||||
self._end_game(channel)
|
||||
else:
|
||||
await ctx.send("You can't surrender when you're not a player")
|
||||
|
||||
def _place_on_board(self, board: dict, player: int, column: int):
|
||||
"""
|
||||
@ -355,17 +426,12 @@ class ConnectFour():
|
||||
else:
|
||||
return board
|
||||
|
||||
def _end_game(self, channel: str):
|
||||
game = self.bot.database["connect 4 games"].find_one({"_id": channel})
|
||||
|
||||
winner = game["winner"]
|
||||
def _end_game(self, winner: int, players: list, difficulty: int):
|
||||
if winner != 0:
|
||||
if game["players"][winner-1] != f"#{self.bot.user.id}":
|
||||
difficulty = int(game["difficulty"])
|
||||
if players[winner-1] != f"#{self.bot.user.id}":
|
||||
difficulty = int(difficulty)
|
||||
reward = difficulty**2 + 5
|
||||
self.bot.money.addMoney(game["players"][winner-1], reward)
|
||||
|
||||
self.database_funcs.delete_game("connect 4 games", channel)
|
||||
self.bot.money.addMoney(f"#{players[winner-1]}", reward)
|
||||
|
||||
def _is_won(self, board: dict):
|
||||
won = 0
|
||||
@ -441,20 +507,19 @@ class ConnectFour():
|
||||
|
||||
return won, win_direction, win_coordinates
|
||||
|
||||
async def _connect_four_ai(self, ctx: SlashContext):
|
||||
async def _connect_four_ai(self,
|
||||
ctx: Union[SlashContext, ComponentContext],
|
||||
board_string: str, players: list,
|
||||
difficulty: int, message_id: int):
|
||||
def out_of_range(possible_scores: list):
|
||||
allowed_range = max(possible_scores)*(1-0.1)
|
||||
more_than_one = len(possible_scores) != 1
|
||||
return (min(possible_scores) <= allowed_range) and more_than_one
|
||||
|
||||
channel = str(ctx.channel.id)
|
||||
|
||||
self.bot.log("Figuring out best move")
|
||||
game = self.bot.database["connect 4 games"].find_one({"_id": channel})
|
||||
|
||||
board = game["board"]
|
||||
player = game["players"].index(f"#{self.bot.user.id}")+1
|
||||
difficulty = game["difficulty"]
|
||||
board = self._decode_board_string(board_string)
|
||||
player = players.index(self.bot.user.id)+1
|
||||
|
||||
scores = [-math.inf for _ in range(COLUMNCOUNT)]
|
||||
for column in range(COLUMNCOUNT):
|
||||
@ -483,7 +548,15 @@ class ConnectFour():
|
||||
best_columns = [i for i, x in enumerate(scores) if x == highest_score]
|
||||
placement = random.choice(best_columns)
|
||||
|
||||
await self.place_piece(ctx, f"#{self.bot.user.id}", placement)
|
||||
await self.place_piece(
|
||||
ctx,
|
||||
board_string,
|
||||
placement,
|
||||
players,
|
||||
difficulty,
|
||||
self.bot.user.id,
|
||||
message_id
|
||||
)
|
||||
|
||||
def _ai_calc_points(self, board: dict, player: int):
|
||||
score = 0
|
||||
@ -683,7 +756,7 @@ class DrawConnectFour():
|
||||
def __init__(self, bot):
|
||||
"""Initialize the class."""
|
||||
self.bot = bot
|
||||
self.database_funcs = self.bot.database_funcs
|
||||
self.get_name = self.bot.database_funcs.get_name
|
||||
win_bar_alpha = 160
|
||||
font_path = "gwendolyn/resources/fonts/futura-bold.ttf"
|
||||
|
||||
@ -729,19 +802,15 @@ class DrawConnectFour():
|
||||
# pylint: enable=invalid-name
|
||||
|
||||
# Draws the whole thing
|
||||
def draw_image(self, channel: str):
|
||||
def draw_image(self, channel: str, board: list, winner: int,
|
||||
win_coordinates: list, win_direction: str, players: list):
|
||||
"""
|
||||
Draw an image of the connect four board.
|
||||
|
||||
*Parameters*
|
||||
------------
|
||||
channel: str
|
||||
The id of the channel the game is in.
|
||||
"""
|
||||
self.bot.log("Drawing connect four board")
|
||||
game = self.bot.database["connect 4 games"].find_one({"_id": channel})
|
||||
|
||||
board = game["board"]
|
||||
|
||||
image_size = (self.WIDTH, self.HEIGHT+self.BOTTOMBORDER)
|
||||
background = Image.new("RGB", image_size, self.BACKGROUNDCOLOR)
|
||||
@ -751,10 +820,10 @@ class DrawConnectFour():
|
||||
|
||||
self._draw_pieces(drawer, board)
|
||||
|
||||
if game["winner"] != 0:
|
||||
self._draw_win(background, game)
|
||||
if winner != 0:
|
||||
self._draw_win(background, win_coordinates, win_direction)
|
||||
|
||||
self._draw_footer(drawer, game)
|
||||
self._draw_footer(drawer, players)
|
||||
|
||||
boards_path = "gwendolyn/resources/games/connect_four_boards/"
|
||||
background.save(boards_path + f"board{channel}.png")
|
||||
@ -897,7 +966,8 @@ class DrawConnectFour():
|
||||
}
|
||||
drawer.ellipse(position, **piece_parameters)
|
||||
|
||||
def _draw_win(self, background: Image, game: dict):
|
||||
def _draw_win(self, background: Image, win_coordinates: list,
|
||||
win_direction: str):
|
||||
"""
|
||||
Draw the bar that shows the winning pieces.
|
||||
|
||||
@ -908,10 +978,9 @@ class DrawConnectFour():
|
||||
game: dict
|
||||
The game data.
|
||||
"""
|
||||
coordinates = game["win coordinates"]
|
||||
start = self.BORDER + self.GRIDBORDER
|
||||
start_x = start + self.PIECEGRIDSIZE[0]*coordinates[1]
|
||||
start_y = start + self.PIECEGRIDSIZE[1]*coordinates[0]
|
||||
start_x = start + self.PIECEGRIDSIZE[0]*win_coordinates[1]
|
||||
start_y = start + self.PIECEGRIDSIZE[1]*win_coordinates[0]
|
||||
diagonal_length = (
|
||||
(math.sqrt(
|
||||
(self.PIECEGRIDSIZE[0]*4-self.GRIDBORDER-self.BORDER)**2+
|
||||
@ -922,7 +991,7 @@ class DrawConnectFour():
|
||||
size_ratio = self.PIECEGRIDSIZE[1]/self.PIECEGRIDSIZE[0]
|
||||
diagonal_angle = math.degrees(math.atan(size_ratio))
|
||||
|
||||
if game["win direction"] == "h":
|
||||
if win_direction == "h":
|
||||
image_size = (self.PIECEGRIDSIZE[0]*4, self.PIECEGRIDSIZE[1])
|
||||
win_bar = Image.new("RGBA", image_size, (0, 0, 0, 0))
|
||||
win_drawer = ImageDraw.Draw(win_bar)
|
||||
@ -942,7 +1011,7 @@ class DrawConnectFour():
|
||||
win_drawer.ellipse(draw_position[1], fill=self.WINBARWHITE)
|
||||
win_drawer.rectangle(draw_position[2], fill=self.WINBARWHITE)
|
||||
|
||||
elif game["win direction"] == "v":
|
||||
elif win_direction == "v":
|
||||
image_size = (self.PIECEGRIDSIZE[0], self.PIECEGRIDSIZE[1]*4)
|
||||
win_bar = Image.new("RGBA", image_size, (0, 0, 0, 0))
|
||||
win_drawer = ImageDraw.Draw(win_bar)
|
||||
@ -962,7 +1031,7 @@ class DrawConnectFour():
|
||||
win_drawer.ellipse(draw_position[1], fill=self.WINBARWHITE)
|
||||
win_drawer.rectangle(draw_position[2], fill=self.WINBARWHITE)
|
||||
|
||||
elif game["win direction"] == "r":
|
||||
elif win_direction == "r":
|
||||
image_width = int(self.PIECEGRIDSIZE[0]*diagonal_length)
|
||||
image_size = (image_width, self.PIECEGRIDSIZE[1])
|
||||
win_bar = Image.new("RGBA", image_size, (0, 0, 0, 0))
|
||||
@ -1001,7 +1070,7 @@ class DrawConnectFour():
|
||||
start_x -= 90
|
||||
start_y -= 100
|
||||
|
||||
elif game["win direction"] == "l":
|
||||
elif win_direction == "l":
|
||||
image_width = int(self.PIECEGRIDSIZE[0]*diagonal_length)
|
||||
image_size = (image_width, self.PIECEGRIDSIZE[1])
|
||||
win_bar = Image.new("RGBA", image_size, (0, 0, 0, 0))
|
||||
@ -1040,16 +1109,16 @@ class DrawConnectFour():
|
||||
win_bar_image = Image.new("RGBA", mask.size, color=self.WINBARCOLOR)
|
||||
background.paste(win_bar_image, (start_x, start_y), mask)
|
||||
|
||||
def _draw_footer(self, drawer: ImageDraw, game: dict):
|
||||
if game["players"][0] == "Gwendolyn":
|
||||
def _draw_footer(self, drawer: ImageDraw, players: list):
|
||||
if players[0] == "Gwendolyn":
|
||||
player1 = "Gwendolyn"
|
||||
else:
|
||||
player1 = self.database_funcs.get_name(game["players"][0])
|
||||
player1 = self.get_name(f"#{players[0]}")
|
||||
|
||||
if game["players"][1] == "Gwendolyn":
|
||||
if players[1] == "Gwendolyn":
|
||||
player2 = "Gwendolyn"
|
||||
else:
|
||||
player2 = self.database_funcs.get_name(game["players"][1])
|
||||
player2 = self.get_name(f"#{players[1]}")
|
||||
|
||||
circle_height = self.HEIGHT - self.BORDER
|
||||
circle_height += (self.BOTTOMBORDER+self.BORDER)//2 - self.TEXTSIZE//2
|
||||
|
@ -14,10 +14,16 @@ import math # Used by DrawHangman(), mainly for drawing circles
|
||||
import random # Used to draw poorly
|
||||
import requests # Used for getting the word in Hangman.start()
|
||||
import discord # Used for discord.file and type hints
|
||||
import os
|
||||
|
||||
from discord_slash.context import SlashContext # Used for typehints
|
||||
from discord_slash.utils.manage_components import (create_button,
|
||||
create_actionrow)
|
||||
from discord_slash.model import ButtonStyle
|
||||
from discord_slash.context import SlashContext, ComponentContext
|
||||
# Used for typehints
|
||||
from PIL import ImageDraw, Image, ImageFont # Used to draw the image
|
||||
|
||||
from gwendolyn.utils import encode_id
|
||||
|
||||
class Hangman():
|
||||
"""
|
||||
@ -43,10 +49,10 @@ class Hangman():
|
||||
APIPARAMS: dict
|
||||
The parameters to pass to every api call.
|
||||
"""
|
||||
self.__bot = bot
|
||||
self.bot = bot
|
||||
self.__draw = DrawHangman(bot)
|
||||
self.__API_url = "https://api.wordnik.com/v4/words.json/randomWords?" # pylint: disable=invalid-name
|
||||
api_key = self.__bot.credentials["wordnik_key"]
|
||||
api_key = self.bot.credentials["wordnik_key"]
|
||||
self.__APIPARAMS = { # pylint: disable=invalid-name
|
||||
"hasDictionaryDef": True,
|
||||
"minCorpusCount": 5000,
|
||||
@ -68,70 +74,88 @@ class Hangman():
|
||||
ctx: SlashContext
|
||||
The context of the command.
|
||||
"""
|
||||
await self.__bot.defer(ctx)
|
||||
channel = str(ctx.channel_id)
|
||||
user = f"#{ctx.author.id}"
|
||||
game = self.__bot.database["hangman games"].find_one({"_id": channel})
|
||||
user_name = self.__bot.database_funcs.get_name(user)
|
||||
started_game = False
|
||||
await self.bot.defer(ctx)
|
||||
|
||||
if game is None:
|
||||
word = "-"
|
||||
while "-" in word or "." in word:
|
||||
response = requests.get(self.__API_url, params=self.__APIPARAMS)
|
||||
word = list(response.json()[0]["word"].upper())
|
||||
word = "-"
|
||||
while "-" in word or "." in word:
|
||||
response = requests.get(self.__API_url, params=self.__APIPARAMS)
|
||||
word = list(response.json()[0]["word"].upper())
|
||||
|
||||
self.__bot.log("Found the word \""+"".join(word)+"\"")
|
||||
guessed = [False] * len(word)
|
||||
game_id = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
|
||||
new_game = {
|
||||
"_id": channel,
|
||||
"player": user,
|
||||
"guessed letters": [],
|
||||
"word": word,
|
||||
"game ID": game_id,
|
||||
"misses": 0,
|
||||
"guessed": guessed
|
||||
}
|
||||
self.__bot.database["hangman games"].insert_one(new_game)
|
||||
self.bot.log("Found the word \""+"".join(word)+"\"")
|
||||
guessed = [False] * len(word)
|
||||
game_id = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
|
||||
|
||||
remaining_letters = list(string.ascii_uppercase)
|
||||
remaining_letters = list(string.ascii_uppercase)
|
||||
|
||||
self.__draw.draw_image(channel)
|
||||
self.__draw.draw_image(game_id, 0, word, guessed, [])
|
||||
|
||||
log_message = "Game started"
|
||||
send_message = f"{user_name} started game of hangman."
|
||||
started_game = True
|
||||
else:
|
||||
log_message = "There was already a game going on"
|
||||
send_message = self.__bot.long_strings["Hangman going on"]
|
||||
send_message = f"{ctx.author.display_name} started a game of hangman."
|
||||
|
||||
self.__bot.log(log_message)
|
||||
await ctx.send(send_message)
|
||||
self.bot.log("Game started")
|
||||
|
||||
if started_game:
|
||||
boards_path = "gwendolyn/resources/games/hangman_boards/"
|
||||
file_path = f"{boards_path}hangman_board{channel}.png"
|
||||
new_image = await ctx.channel.send(file=discord.File(file_path))
|
||||
boards_path = "gwendolyn/resources/games/hangman_boards/"
|
||||
file_path = f"{boards_path}hangman_board{game_id}.png"
|
||||
image_message = await ctx.send(
|
||||
send_message,
|
||||
file=discord.File(file_path)
|
||||
)
|
||||
|
||||
blank_message = await ctx.channel.send("_ _")
|
||||
reaction_messages = {
|
||||
new_image: remaining_letters[:15],
|
||||
blank_message: remaining_letters[15:]
|
||||
}
|
||||
blank_message_one = await ctx.channel.send("_ _")
|
||||
blank_message_two = await ctx.channel.send("_ _")
|
||||
|
||||
old_messages = f"{new_image.id}\n{blank_message.id}"
|
||||
old_images_path = "gwendolyn/resources/games/old_images/"
|
||||
buttons = []
|
||||
for letter in remaining_letters:
|
||||
custom_id = encode_id([
|
||||
"hangman",
|
||||
"guess",
|
||||
str(ctx.author.id),
|
||||
letter,
|
||||
"".join(word),
|
||||
"",
|
||||
game_id,
|
||||
str(image_message.id),
|
||||
str(blank_message_one.id),
|
||||
str(blank_message_two.id)
|
||||
])
|
||||
buttons.append(create_button(
|
||||
style=ButtonStyle.blue,
|
||||
label=letter,
|
||||
custom_id=custom_id
|
||||
))
|
||||
|
||||
with open(old_images_path+f"hangman{channel}", "w") as file_pointer:
|
||||
file_pointer.write(old_messages)
|
||||
custom_id = encode_id([
|
||||
"hangman",
|
||||
"end",
|
||||
str(ctx.author.id),
|
||||
game_id,
|
||||
str(image_message.id),
|
||||
str(blank_message_one.id),
|
||||
str(blank_message_two.id)
|
||||
])
|
||||
buttons.append(create_button(
|
||||
style=ButtonStyle.red,
|
||||
label="End game",
|
||||
custom_id=custom_id
|
||||
))
|
||||
|
||||
for message, letters in reaction_messages.items():
|
||||
for letter in letters:
|
||||
emoji = chr(ord(letter)+127397)
|
||||
await message.add_reaction(emoji)
|
||||
action_row_lists = []
|
||||
for i in range(((len(buttons)-1)//20)+1):
|
||||
action_rows = []
|
||||
available_buttons = buttons[(i*20):(min(len(buttons),i*20+20))]
|
||||
for x in range(((len(available_buttons)-1)//4)+1):
|
||||
row_buttons = available_buttons[
|
||||
(x*4):(min(len(available_buttons),x*4+4))
|
||||
]
|
||||
action_rows.append(create_actionrow(*row_buttons))
|
||||
action_row_lists.append(action_rows)
|
||||
|
||||
async def stop(self, ctx: SlashContext):
|
||||
|
||||
await blank_message_one.edit(components=action_row_lists[0])
|
||||
await blank_message_two.edit(components=action_row_lists[1])
|
||||
|
||||
|
||||
async def stop(self, ctx: ComponentContext, game_id: str,
|
||||
*messages: list[str]):
|
||||
"""
|
||||
Stop the game of hangman.
|
||||
|
||||
@ -140,28 +164,20 @@ class Hangman():
|
||||
ctx: SlashContext
|
||||
The context of the command.
|
||||
"""
|
||||
channel = str(ctx.channel.id)
|
||||
game = self.__bot.database["hangman games"].find_one({"_id": channel})
|
||||
|
||||
if game is None:
|
||||
await ctx.send("There's no game going on")
|
||||
elif f"#{ctx.author.id}" != game["player"]:
|
||||
await ctx.send("You can't end a game you're not in")
|
||||
else:
|
||||
self.__bot.database["hangman games"].delete_one({"_id": channel})
|
||||
old_images_path = "gwendolyn/resources/games/old_images/"
|
||||
self.bot.log("Deleting old messages")
|
||||
|
||||
with open(old_images_path+f"hangman{channel}", "r") as file_pointer:
|
||||
messages = file_pointer.read().splitlines()
|
||||
await ctx.channel.send("Hangman game stopped")
|
||||
boards_path = "gwendolyn/resources/games/hangman_boards/"
|
||||
file_path = f"{boards_path}hangman_board{game_id}.png"
|
||||
os.remove(file_path)
|
||||
|
||||
for message in messages:
|
||||
old_message = await ctx.channel.fetch_message(int(message))
|
||||
self.__bot.log("Deleting old message")
|
||||
await old_message.delete()
|
||||
for msg_id in messages:
|
||||
msg = await ctx.channel.fetch_message(msg_id)
|
||||
await msg.delete()
|
||||
|
||||
await ctx.send("Game stopped")
|
||||
|
||||
async def guess(self, message: discord.Message, user: str, guess: str):
|
||||
async def guess(self, ctx: ComponentContext, guess: str, word: str,
|
||||
guessed_letters: str, game_id: str, *messages: list[str]):
|
||||
"""
|
||||
Guess a letter.
|
||||
|
||||
@ -174,98 +190,113 @@ class Hangman():
|
||||
guess: str
|
||||
The guess.
|
||||
"""
|
||||
channel = str(message.channel.id)
|
||||
hangman_games = self.__bot.database["hangman games"]
|
||||
game = hangman_games.find_one({"_id": channel})
|
||||
|
||||
game_exists = (game is not None)
|
||||
single_letter = (len(guess) == 1 and guess.isalpha())
|
||||
new_guess = (guess not in game["guessed letters"])
|
||||
valid_guess = (game_exists and single_letter and new_guess)
|
||||
|
||||
if valid_guess:
|
||||
self.__bot.log("Guessed the letter")
|
||||
misses = 0
|
||||
guessed_letters += guess
|
||||
guessed = [False for i in range(len(word))]
|
||||
for guessed_letter in guessed_letters:
|
||||
correct_guess = 0
|
||||
|
||||
for i, letter in enumerate(game["word"]):
|
||||
if guess == letter:
|
||||
for i, letter in enumerate(word):
|
||||
if guessed_letter == letter:
|
||||
correct_guess += 1
|
||||
updater = {"$set": {f"guessed.{i}": True}}
|
||||
hangman_games.update_one({"_id": channel}, updater)
|
||||
guessed[i] = True
|
||||
|
||||
if correct_guess == 0:
|
||||
updater = {"$inc": {"misses": 1}}
|
||||
hangman_games.update_one({"_id": channel}, updater)
|
||||
misses += 1
|
||||
|
||||
updater = {"$push": {"guessed letters": guess}}
|
||||
hangman_games.update_one({"_id": channel}, updater)
|
||||
|
||||
remaining_letters = list(string.ascii_uppercase)
|
||||
remaining_letters = list(string.ascii_uppercase)
|
||||
for letter in guessed_letters:
|
||||
remaining_letters.remove(letter)
|
||||
|
||||
game = hangman_games.find_one({"_id": channel})
|
||||
if correct_guess == 1:
|
||||
send_message = "Guessed {}. There was 1 {} in the word."
|
||||
send_message = send_message.format(guess, guess)
|
||||
else:
|
||||
send_message = "Guessed {}. There were {} {}s in the word."
|
||||
send_message = send_message.format(guess, correct_guess, guess)
|
||||
|
||||
for letter in game["guessed letters"]:
|
||||
remaining_letters.remove(letter)
|
||||
self.__draw.draw_image(game_id, misses, word, guessed, guessed_letters)
|
||||
|
||||
if correct_guess == 1:
|
||||
send_message = "Guessed {}. There was 1 {} in the word."
|
||||
send_message = send_message.format(guess, guess)
|
||||
else:
|
||||
send_message = "Guessed {}. There were {} {}s in the word."
|
||||
send_message = send_message.format(guess, correct_guess, guess)
|
||||
if misses == 6:
|
||||
send_message += self.bot.long_strings["Hangman lost game"]
|
||||
remaining_letters = []
|
||||
elif all(guessed):
|
||||
self.bot.money.addMoney(f"#{ctx.author_id}", 15)
|
||||
send_message += self.bot.long_strings["Hangman guessed word"]
|
||||
remaining_letters = []
|
||||
|
||||
self.__draw.draw_image(channel)
|
||||
|
||||
if game["misses"] == 6:
|
||||
hangman_games.delete_one({"_id": channel})
|
||||
send_message += self.__bot.long_strings["Hangman lost game"]
|
||||
remaining_letters = []
|
||||
elif all(game["guessed"]):
|
||||
hangman_games.delete_one({"_id": channel})
|
||||
self.__bot.money.addMoney(user, 15)
|
||||
send_message += self.__bot.long_strings["Hangman guessed word"]
|
||||
remaining_letters = []
|
||||
|
||||
await message.channel.send(send_message)
|
||||
old_images_path = "gwendolyn/resources/games/old_images/"
|
||||
|
||||
with open(old_images_path+f"hangman{channel}", "r") as file_pointer:
|
||||
old_message_ids = file_pointer.read().splitlines()
|
||||
|
||||
for old_id in old_message_ids:
|
||||
old_message = await message.channel.fetch_message(int(old_id))
|
||||
self.__bot.log("Deleting old message")
|
||||
await old_message.delete()
|
||||
|
||||
if remaining_letters != []:
|
||||
boards_path = "gwendolyn/resources/games/hangman_boards/"
|
||||
file_path = f"{boards_path}hangman_board{channel}.png"
|
||||
new_image = await message.channel.send(file=discord.File(file_path))
|
||||
file_path = f"{boards_path}hangman_board{game_id}.png"
|
||||
image_message = await ctx.channel.send(
|
||||
send_message,
|
||||
file=discord.File(file_path)
|
||||
)
|
||||
blank_message_one = await ctx.channel.send("_ _")
|
||||
blank_message_two = await ctx.channel.send("_ _")
|
||||
buttons = []
|
||||
for letter in list(string.ascii_uppercase):
|
||||
custom_id = encode_id([
|
||||
"hangman",
|
||||
"guess",
|
||||
str(ctx.author.id),
|
||||
letter,
|
||||
word,
|
||||
guessed_letters,
|
||||
game_id,
|
||||
str(image_message.id),
|
||||
str(blank_message_one.id),
|
||||
str(blank_message_two.id)
|
||||
])
|
||||
buttons.append(create_button(
|
||||
style=ButtonStyle.blue,
|
||||
label=letter,
|
||||
custom_id=custom_id,
|
||||
disabled=(letter not in remaining_letters)
|
||||
))
|
||||
custom_id = encode_id([
|
||||
"hangman",
|
||||
"end",
|
||||
str(ctx.author.id),
|
||||
game_id,
|
||||
str(image_message.id),
|
||||
str(blank_message_one.id),
|
||||
str(blank_message_two.id)
|
||||
])
|
||||
buttons.append(create_button(
|
||||
style=ButtonStyle.red,
|
||||
label="End game",
|
||||
custom_id=custom_id
|
||||
))
|
||||
|
||||
if len(remaining_letters) > 0:
|
||||
if len(remaining_letters) > 15:
|
||||
blank_message = await message.channel.send("_ _")
|
||||
reaction_messages = {
|
||||
new_image: remaining_letters[:15],
|
||||
blank_message: remaining_letters[15:]
|
||||
}
|
||||
else:
|
||||
blank_message = ""
|
||||
reaction_messages = {new_image: remaining_letters}
|
||||
action_row_lists = []
|
||||
for i in range(((len(buttons)-1)//20)+1):
|
||||
action_rows = []
|
||||
available_buttons = buttons[(i*20):(min(len(buttons),i*20+20))]
|
||||
for x in range(((len(available_buttons)-1)//4)+1):
|
||||
row_buttons = available_buttons[
|
||||
(x*4):(min(len(available_buttons),x*4+4))
|
||||
]
|
||||
action_rows.append(create_actionrow(*row_buttons))
|
||||
action_row_lists.append(action_rows)
|
||||
|
||||
await blank_message_one.edit(components=action_row_lists[0])
|
||||
await blank_message_two.edit(components=action_row_lists[1])
|
||||
else:
|
||||
boards_path = "gwendolyn/resources/games/hangman_boards/"
|
||||
file_path = f"{boards_path}hangman_board{game_id}.png"
|
||||
await ctx.channel.send(send_message, file=discord.File(file_path))
|
||||
|
||||
os.remove(file_path)
|
||||
|
||||
for msg_id in messages:
|
||||
msg = await ctx.channel.fetch_message(msg_id)
|
||||
await msg.delete()
|
||||
|
||||
if blank_message != "":
|
||||
old_messages = f"{new_image.id}\n{blank_message.id}"
|
||||
else:
|
||||
old_messages = str(new_image.id)
|
||||
|
||||
old_images_path = "gwendolyn/resources/games/old_images/"
|
||||
old_image_path = old_images_path + f"hangman{channel}"
|
||||
with open(old_image_path, "w") as file_pointer:
|
||||
file_pointer.write(old_messages)
|
||||
|
||||
for message, letters in reaction_messages.items():
|
||||
for letter in letters:
|
||||
emoji = chr(ord(letter)+127397)
|
||||
await message.add_reaction(emoji)
|
||||
|
||||
|
||||
class DrawHangman():
|
||||
@ -355,9 +386,9 @@ class DrawHangman():
|
||||
deviance_accuracy_x = 0
|
||||
deviance_accuracy_y = 0
|
||||
start = random.randint(-100, -80)
|
||||
degreess_amount = 360 + random.randint(-10, 30)
|
||||
degrees_amount = 360 + random.randint(-10, 30)
|
||||
|
||||
for degree in range(degreess_amount):
|
||||
for degree in range(degrees_amount):
|
||||
deviance_x, deviance_accuracy_x = self.__deviate(
|
||||
deviance_x,
|
||||
deviance_accuracy_x,
|
||||
@ -589,7 +620,8 @@ class DrawHangman():
|
||||
placed = True
|
||||
return background
|
||||
|
||||
def draw_image(self, channel: str):
|
||||
def draw_image(self, game_id: str, misses: int, word: str,
|
||||
guessed: list[bool], guessed_letters: str):
|
||||
"""
|
||||
Draw a hangman Image.
|
||||
|
||||
@ -598,21 +630,20 @@ class DrawHangman():
|
||||
channel: str
|
||||
The id of the channel the game is in.
|
||||
"""
|
||||
self.__bot.log("Drawing hangman image", channel)
|
||||
game = self.__bot.database["hangman games"].find_one({"_id": channel})
|
||||
self.__bot.log("Drawing hangman image")
|
||||
|
||||
random.seed(game["game ID"])
|
||||
random.seed(game_id)
|
||||
|
||||
background = Image.open("gwendolyn/resources/paper.jpg")
|
||||
gallow = self.__draw_gallows()
|
||||
man = self.__draw_man(game["misses"], game["game ID"])
|
||||
man = self.__draw_man(misses, game_id)
|
||||
|
||||
random.seed(game["game ID"])
|
||||
letter_line_parameters = [game["word"], game["guessed"], game["misses"]]
|
||||
random.seed(game_id)
|
||||
letter_line_parameters = [word, guessed, misses]
|
||||
letter_lines = self.__draw_letter_lines(*letter_line_parameters)
|
||||
|
||||
random.seed(game["game ID"])
|
||||
misses = self.__draw_misses(game["guessed letters"], game["word"])
|
||||
random.seed(game_id)
|
||||
misses = self.__draw_misses(list(guessed_letters), word)
|
||||
|
||||
background.paste(gallow, (100, 100), gallow)
|
||||
background.paste(man, (300, 210), man)
|
||||
@ -623,5 +654,6 @@ class DrawHangman():
|
||||
misses_text_width = misses_text.size[0]
|
||||
background.paste(misses_text, (850-misses_text_width//2, 50), misses_text)
|
||||
|
||||
board_path = f"gwendolyn/resources/games/hangman_boards/hangman_board{channel}.png"
|
||||
background.save(board_path)
|
||||
boards_path = "gwendolyn/resources/games/hangman_boards/"
|
||||
image_path = f"{boards_path}hangman_board{game_id}.png"
|
||||
background.save(image_path)
|
||||
|
@ -1,11 +1,17 @@
|
||||
"""Plex integration with the bot."""
|
||||
from math import floor, ceil
|
||||
import time
|
||||
import json
|
||||
import asyncio
|
||||
import requests
|
||||
import imdb
|
||||
import discord
|
||||
import xmltodict
|
||||
|
||||
from discord_slash.utils.manage_components import (create_button,
|
||||
create_actionrow)
|
||||
from discord_slash.model import ButtonStyle
|
||||
|
||||
from gwendolyn.utils import encode_id
|
||||
|
||||
class Plex():
|
||||
"""Container for Plex functions and commands."""
|
||||
@ -64,88 +70,102 @@ class Plex():
|
||||
colour=0x00FF00
|
||||
)
|
||||
|
||||
message = await ctx.send(embed=embed)
|
||||
|
||||
message_data = {"message_id":message.id,"imdb_ids":imdb_ids}
|
||||
|
||||
file_path = f"gwendolyn/resources/plex/old_message{ctx.channel.id}"
|
||||
with open(file_path,"w") as file_pointer:
|
||||
json.dump(message_data, file_pointer)
|
||||
|
||||
buttons = []
|
||||
if len(movies) == 1:
|
||||
await message.add_reaction("✔️")
|
||||
buttons.append(create_button(
|
||||
style=ButtonStyle.green,
|
||||
label="✓",
|
||||
custom_id=encode_id(["plex", "movie", str(imdb_ids[0])])
|
||||
)
|
||||
)
|
||||
else:
|
||||
for i in range(len(movies)):
|
||||
await message.add_reaction(["1️⃣","2️⃣","3️⃣","4️⃣","5️⃣"][i])
|
||||
buttons.append(
|
||||
create_button(
|
||||
style=ButtonStyle.blue,
|
||||
label=str(i+1),
|
||||
custom_id=encode_id(["plex", "movie", str(imdb_ids[i])])
|
||||
)
|
||||
)
|
||||
|
||||
buttons.append(create_button(
|
||||
style=ButtonStyle.red,
|
||||
label="X",
|
||||
custom_id=encode_id(["plex", "movie", ""])
|
||||
)
|
||||
)
|
||||
|
||||
action_rows = []
|
||||
for i in range(((len(buttons)-1)//5)+1):
|
||||
action_rows.append(
|
||||
create_actionrow(
|
||||
*buttons[(i*5):(min(len(buttons),i*5+5))]
|
||||
)
|
||||
)
|
||||
|
||||
await ctx.send(embed=embed, components=action_rows)
|
||||
|
||||
await message.add_reaction("❌")
|
||||
|
||||
message = await ctx.channel.fetch_message(message.id)
|
||||
if (message.content != "" and
|
||||
not isinstance(ctx.channel, discord.DMChannel)):
|
||||
await message.clear_reactions()
|
||||
|
||||
|
||||
async def add_movie(self, message, imdb_id, edit_message = True):
|
||||
"""Add a movie to Plex server."""
|
||||
if imdb_id is None:
|
||||
|
||||
if not edit_message:
|
||||
await message.delete()
|
||||
|
||||
if imdb_id == "":
|
||||
self.bot.log("Did not find what the user was searching for")
|
||||
if edit_message:
|
||||
await message.edit(
|
||||
embed = None,
|
||||
content = "Try searching for the IMDB id"
|
||||
)
|
||||
else:
|
||||
await message.channel.send("Try searching for the IMDB id")
|
||||
message_text = "Try searching for the IMDB id"
|
||||
else:
|
||||
self.bot.log("Trying to add movie "+str(imdb_id))
|
||||
|
||||
# Searches for the movie using the imdb id through Radarr
|
||||
api_key = self.credentials["radarr_key"]
|
||||
request_url = self.radarr_url+"movie/lookup/imdb?imdbId=tt"+imdb_id
|
||||
request_url += "&apiKey="+api_key
|
||||
response = requests.get(request_url)
|
||||
|
||||
# Makes the dict used for the post request
|
||||
lookup_data = response.json()
|
||||
post_data = {"qualityProfileId": 1,
|
||||
"rootFolder_path" : self.movie_path,
|
||||
"monitored" : True,
|
||||
"addOptions": {"searchForMovie": True}}
|
||||
post_data = {
|
||||
"qualityProfileId": 1,
|
||||
"rootFolderPath" : self.movie_path,
|
||||
"monitored" : True,
|
||||
"addOptions": {"searchForMovie": True}
|
||||
}
|
||||
for key in ["tmdbId","title","titleSlug","images","year"]:
|
||||
post_data.update({key : lookup_data[key]})
|
||||
|
||||
# Makes the post request
|
||||
response = requests.post(
|
||||
url= self.radarr_url+"movie?apikey="+api_key,
|
||||
url = self.radarr_url+"movie?apikey="+api_key,
|
||||
json = post_data
|
||||
)
|
||||
|
||||
# Deciphers the response
|
||||
if response.status_code == 201:
|
||||
success_message = "{} successfully added to Plex".format(
|
||||
self.bot.log("Added "+post_data["title"]+" to Plex")
|
||||
message_text = "{} successfully added to Plex".format(
|
||||
post_data["title"]
|
||||
)
|
||||
if edit_message:
|
||||
await message.edit(
|
||||
embed = None,
|
||||
content = success_message
|
||||
)
|
||||
else:
|
||||
await message.channel.send(success_message)
|
||||
|
||||
self.bot.log("Added "+post_data["title"]+" to Plex")
|
||||
elif response.status_code == 400:
|
||||
fail_text = self.long_strings["Already on Plex"].format(
|
||||
self.bot.log("The movie was already on plex")
|
||||
message_text = self.long_strings["Already on Plex"].format(
|
||||
post_data['title']
|
||||
)
|
||||
if edit_message:
|
||||
await message.edit(embed = None, content = fail_text)
|
||||
else:
|
||||
await message.channel.send(fail_text)
|
||||
else:
|
||||
if edit_message:
|
||||
await message.edit(
|
||||
embed = None,
|
||||
content = "Something went wrong"
|
||||
)
|
||||
else:
|
||||
await message.channel.send("Something went wrong")
|
||||
self.bot.log(str(response.status_code)+" "+response.reason)
|
||||
message_text = "Something went wrong",
|
||||
|
||||
if edit_message:
|
||||
await message.edit(
|
||||
embed = None,
|
||||
content = message_text,
|
||||
components = []
|
||||
)
|
||||
else:
|
||||
await message.channel.send(message_text)
|
||||
|
||||
async def request_show(self, ctx, show_name):
|
||||
"""Request a show for the Plex server."""
|
||||
@ -166,7 +186,7 @@ class Plex():
|
||||
message_title = "**Is it any of these shows?**"
|
||||
|
||||
message_text = ""
|
||||
imdb_names = []
|
||||
imdb_ids = []
|
||||
|
||||
for i, show in enumerate(shows):
|
||||
try:
|
||||
@ -176,10 +196,10 @@ class Plex():
|
||||
message_text += "\n"+str(i+1)+") "+show["title"]
|
||||
except KeyError:
|
||||
message_text += "Error"
|
||||
imdb_names.append(show["title"])
|
||||
imdb_ids.append(show.movieID)
|
||||
|
||||
self.bot.log(
|
||||
f"Returning a list of {len(shows)} possible shows: {imdb_names}"
|
||||
f"Returning a list of {len(shows)} possible shows: {imdb_ids}"
|
||||
)
|
||||
|
||||
embed = discord.Embed(
|
||||
@ -188,42 +208,69 @@ class Plex():
|
||||
colour=0x00FF00
|
||||
)
|
||||
|
||||
message = await ctx.send(embed=embed)
|
||||
|
||||
message_data = {"message_id":message.id,"imdb_names":imdb_names}
|
||||
|
||||
file_path = "gwendolyn/resources/plex/old_message"+str(ctx.channel.id)
|
||||
with open(file_path,"w") as file_pointer:
|
||||
json.dump(message_data, file_pointer)
|
||||
|
||||
buttons = []
|
||||
if len(shows) == 1:
|
||||
await message.add_reaction("✔️")
|
||||
else:
|
||||
for i in range(len(shows)):
|
||||
await message.add_reaction(["1️⃣","2️⃣","3️⃣","4️⃣","5️⃣"][i])
|
||||
|
||||
await message.add_reaction("❌")
|
||||
|
||||
message = await ctx.channel.fetch_message(message.id)
|
||||
if message.content != "":
|
||||
if not isinstance(ctx.channel, discord.DMChannel):
|
||||
await message.clear_reactions()
|
||||
|
||||
async def add_show(self, message, imdb_name):
|
||||
"""Add the requested show to Plex."""
|
||||
if imdb_name is None:
|
||||
self.bot.log("Did not find what the user was searching for")
|
||||
await message.edit(
|
||||
embed = None,
|
||||
content = "Try searching for the IMDB id"
|
||||
buttons.append(create_button(
|
||||
style=ButtonStyle.green,
|
||||
label="✓",
|
||||
custom_id=encode_id(["plex", "show", str(imdb_ids[0])])
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.bot.log("Trying to add show "+str(imdb_name))
|
||||
for i in range(len(shows)):
|
||||
buttons.append(
|
||||
create_button(
|
||||
style=ButtonStyle.blue,
|
||||
label=str(i+1),
|
||||
custom_id=encode_id(["plex", "show", str(imdb_ids[i])])
|
||||
)
|
||||
)
|
||||
|
||||
buttons.append(create_button(
|
||||
style=ButtonStyle.red,
|
||||
label="X",
|
||||
custom_id=encode_id(["plex", "show", ""])
|
||||
)
|
||||
)
|
||||
|
||||
action_rows = []
|
||||
for i in range(((len(buttons)-1)//5)+1):
|
||||
action_rows.append(
|
||||
create_actionrow(
|
||||
*buttons[(i*5):(min(len(buttons),i*5+5))]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
await ctx.send(embed=embed, components=action_rows)
|
||||
|
||||
async def add_show(self, message, imdb_id, edit_message = True):
|
||||
"""Add the requested show to Plex."""
|
||||
if imdb_id == "":
|
||||
self.bot.log("Did not find what the user was searching for")
|
||||
message_text = "Try searching for the IMDB id"
|
||||
else:
|
||||
self.bot.log("Trying to add show "+str(imdb_id))
|
||||
|
||||
# Finds the tvdb id
|
||||
tvdb_api_url = "https://thetvdb.com/api/"
|
||||
tvdb_method = "GetSeriesByRemoteID.php"
|
||||
tvdb_request_url = f"{tvdb_api_url}{tvdb_method}"
|
||||
tvdb_id = xmltodict.parse(
|
||||
requests.get(
|
||||
tvdb_request_url+f"?imdbid=tt{imdb_id}",
|
||||
headers = {"ContentType" : "application/json"}
|
||||
).text
|
||||
)['Data']['Series']['seriesid']
|
||||
|
||||
# Finds the rest of the information using Sonarr
|
||||
api_key = self.credentials["sonarr_key"]
|
||||
request_url = self.sonarr_url+"series/lookup?term="
|
||||
request_url += imdb_name.replace(" ","%20")
|
||||
request_url += f"tvdb:{tvdb_id}"
|
||||
request_url += "&apiKey="+api_key
|
||||
response = requests.get(request_url)
|
||||
|
||||
# Makes the dict used for the post request
|
||||
lookup_data = response.json()[0]
|
||||
post_data = {
|
||||
"ProfileId" : 1,
|
||||
@ -234,29 +281,32 @@ class Plex():
|
||||
for key in ["tvdbId","title","titleSlug","images","seasons"]:
|
||||
post_data.update({key : lookup_data[key]})
|
||||
|
||||
|
||||
# Makes the post request
|
||||
response = requests.post(
|
||||
url= self.sonarr_url+"series?apikey="+api_key,
|
||||
json = post_data
|
||||
)
|
||||
|
||||
# Deciphers the response
|
||||
if response.status_code == 201:
|
||||
await message.edit(
|
||||
embed = None,
|
||||
content = post_data["title"]+" successfully added to Plex"
|
||||
)
|
||||
self.bot.log("Added a "+post_data["title"]+" to Plex")
|
||||
message_text = post_data["title"]+" successfully added to Plex"
|
||||
elif response.status_code == 400:
|
||||
text = self.long_strings["Already on Plex"].format(
|
||||
message_text = self.long_strings["Already on Plex"].format(
|
||||
post_data['title']
|
||||
)
|
||||
await message.edit(embed = None, content = text)
|
||||
else:
|
||||
await message.edit(
|
||||
embed = None,
|
||||
content = "Something went wrong"
|
||||
)
|
||||
self.bot.log(str(response.status_code)+" "+response.reason)
|
||||
message_text = "Something went wrong"
|
||||
|
||||
if edit_message:
|
||||
await message.edit(
|
||||
embed = None,
|
||||
content = message_text,
|
||||
components = []
|
||||
)
|
||||
else:
|
||||
await message.channel.send(message_text)
|
||||
|
||||
async def __generate_download_list(self, show_dm, show_movies, show_shows,
|
||||
episodes):
|
||||
@ -364,7 +414,9 @@ class Plex():
|
||||
movie_list = requests.get(
|
||||
self.radarr_url+"movie?apiKey="+self.credentials["radarr_key"]
|
||||
).json()
|
||||
print(self.radarr_url+"movie?apiKey="+self.credentials["radarr_key"])
|
||||
print(
|
||||
self.radarr_url+"movie?apiKey="+self.credentials["radarr_key"]
|
||||
)
|
||||
movie_queue = requests.get(
|
||||
self.radarr_url+"queue?apiKey="+self.credentials["radarr_key"]
|
||||
).json()
|
||||
|
@ -1,135 +1,135 @@
|
||||
"""
|
||||
Contains the Gwendolyn class, a subclass of the discord command bot.
|
||||
|
||||
*Classes*
|
||||
---------
|
||||
Gwendolyn(discord.ext.commands.Bot)
|
||||
"""
|
||||
import os # Used for loading cogs in Gwendolyn.addCogs
|
||||
import finnhub # Used to add a finhub client to the bot
|
||||
import discord # Used for discord.Intents and discord.Status
|
||||
import discord_slash # Used to initialized SlashCommands object
|
||||
import git # Used to pull when stopping
|
||||
|
||||
from discord.ext import commands # Used to inherit from commands.bot
|
||||
from pymongo import MongoClient # Used for database management
|
||||
from gwendolyn.funcs import Money, StarWars, Games, Other, LookupFuncs
|
||||
from gwendolyn.utils import (get_options, get_credentials, log_this,
|
||||
DatabaseFuncs, EventHandler, ErrorHandler,
|
||||
long_strings)
|
||||
|
||||
|
||||
class Gwendolyn(commands.Bot):
|
||||
"""
|
||||
A multifunctional Discord bot.
|
||||
|
||||
*Methods*
|
||||
---------
|
||||
log(messages: Union[str, list], channel: str = "",
|
||||
level: int = 20)
|
||||
stop(ctx: discord_slash.context.SlashContext)
|
||||
defer(ctx: discord_slash.context.SlashContext)
|
||||
"""
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the bot."""
|
||||
intents = discord.Intents.default()
|
||||
intents.members = True
|
||||
initiation_parameters = {
|
||||
"command_prefix": " ",
|
||||
"case_insensitive": True,
|
||||
"intents": intents,
|
||||
"status": discord.Status.dnd
|
||||
}
|
||||
super().__init__(**initiation_parameters)
|
||||
|
||||
self._add_clients_and_options()
|
||||
self._add_util_classes()
|
||||
self._add_function_containers()
|
||||
self._add_cogs()
|
||||
|
||||
def _add_clients_and_options(self):
|
||||
"""Add all the client, option and credentials objects."""
|
||||
self.long_strings = long_strings()
|
||||
self.options = get_options()
|
||||
self.credentials = get_credentials()
|
||||
finnhub_key = self.credentials["finnhub_key"]
|
||||
self.finnhub_client = finnhub.Client(api_key=finnhub_key)
|
||||
mongo_user = self.credentials["mongo_db_user"]
|
||||
mongo_password = self.credentials["mongo_db_password"]
|
||||
mongo_url = f"mongodb+srv://{mongo_user}:{mongo_password}@gwendolyn"
|
||||
mongo_url += ".qkwfy.mongodb.net/Gwendolyn?retryWrites=true&w=majority"
|
||||
database_clint = MongoClient(mongo_url)
|
||||
|
||||
if self.options["testing"]:
|
||||
self.log("Testing mode")
|
||||
self.database = database_clint["Gwendolyn-Test"]
|
||||
else:
|
||||
self.database = database_clint["Gwendolyn"]
|
||||
|
||||
def _add_util_classes(self):
|
||||
"""Add all the classes used as utility."""
|
||||
self.database_funcs = DatabaseFuncs(self)
|
||||
self.event_handler = EventHandler(self)
|
||||
self.error_handler = ErrorHandler(self)
|
||||
slash_parameters = {
|
||||
"sync_commands": True,
|
||||
"sync_on_cog_reload": True,
|
||||
"override_type": True
|
||||
}
|
||||
self.slash = discord_slash.SlashCommand(self, **slash_parameters)
|
||||
|
||||
def _add_function_containers(self):
|
||||
"""Add all the function containers used for commands."""
|
||||
self.star_wars = StarWars(self)
|
||||
self.other = Other(self)
|
||||
self.lookup_funcs = LookupFuncs(self)
|
||||
self.games = Games(self)
|
||||
self.money = Money(self)
|
||||
|
||||
def _add_cogs(self):
|
||||
"""Load cogs."""
|
||||
for filename in os.listdir("./gwendolyn/cogs"):
|
||||
if filename.endswith(".py"):
|
||||
self.load_extension(f"gwendolyn.cogs.{filename[:-3]}")
|
||||
|
||||
def log(self, messages, channel: str = "", level: int = 20):
|
||||
"""Log a message. Described in utils/util_functions.py."""
|
||||
log_this(messages, channel, level)
|
||||
|
||||
async def stop(self, ctx: discord_slash.context.SlashContext):
|
||||
"""
|
||||
Stop the bot, and stop running games.
|
||||
|
||||
Only stops the bot if the user in ctx.author is one of the
|
||||
admins given in options.txt.
|
||||
|
||||
*parameters*
|
||||
------------
|
||||
ctx: discord_slash.context.SlashContext
|
||||
The context of the "/stop" slash command.
|
||||
"""
|
||||
if f"#{ctx.author.id}" in self.options["admins"]:
|
||||
await ctx.send("Pulling git repo and restarting...")
|
||||
|
||||
await self.change_presence(status=discord.Status.offline)
|
||||
|
||||
self.database_funcs.wipe_games()
|
||||
if not self.options["testing"]:
|
||||
git_client = git.cmd.Git("")
|
||||
git_client.pull()
|
||||
|
||||
self.log("Logging out", level=25)
|
||||
await self.close()
|
||||
else:
|
||||
log_message = f"{ctx.author.display_name} tried to stop me!"
|
||||
self.log(log_message, str(ctx.channel_id))
|
||||
await ctx.send(f"I don't think I will, {ctx.author.display_name}")
|
||||
|
||||
async def defer(self, ctx: discord_slash.context.SlashContext):
|
||||
"""Send a "Gwendolyn is thinking" message to the user."""
|
||||
try:
|
||||
await ctx.defer()
|
||||
except discord_slash.error.AlreadyResponded:
|
||||
self.log("defer failed")
|
||||
"""
|
||||
Contains the Gwendolyn class, a subclass of the discord command bot.
|
||||
|
||||
*Classes*
|
||||
---------
|
||||
Gwendolyn(discord.ext.commands.Bot)
|
||||
"""
|
||||
import os # Used for loading cogs in Gwendolyn.addCogs
|
||||
import finnhub # Used to add a finhub client to the bot
|
||||
import discord # Used for discord.Intents and discord.Status
|
||||
import discord_slash # Used to initialized SlashCommands object
|
||||
import git # Used to pull when stopping
|
||||
|
||||
from discord.ext import commands # Used to inherit from commands.bot
|
||||
from pymongo import MongoClient # Used for database management
|
||||
from gwendolyn.funcs import Money, StarWars, Games, Other, LookupFuncs
|
||||
from gwendolyn.utils import (get_options, get_credentials, log_this,
|
||||
DatabaseFuncs, EventHandler, ErrorHandler,
|
||||
long_strings)
|
||||
|
||||
|
||||
class Gwendolyn(commands.Bot):
|
||||
"""
|
||||
A multifunctional Discord bot.
|
||||
|
||||
*Methods*
|
||||
---------
|
||||
log(messages: Union[str, list], channel: str = "",
|
||||
level: int = 20)
|
||||
stop(ctx: discord_slash.context.SlashContext)
|
||||
defer(ctx: discord_slash.context.SlashContext)
|
||||
"""
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the bot."""
|
||||
intents = discord.Intents.default()
|
||||
intents.members = True
|
||||
initiation_parameters = {
|
||||
"command_prefix": " ",
|
||||
"case_insensitive": True,
|
||||
"intents": intents,
|
||||
"status": discord.Status.dnd
|
||||
}
|
||||
super().__init__(**initiation_parameters)
|
||||
|
||||
self._add_clients_and_options()
|
||||
self._add_util_classes()
|
||||
self._add_function_containers()
|
||||
self._add_cogs()
|
||||
|
||||
def _add_clients_and_options(self):
|
||||
"""Add all the client, option and credentials objects."""
|
||||
self.long_strings = long_strings()
|
||||
self.options = get_options()
|
||||
self.credentials = get_credentials()
|
||||
finnhub_key = self.credentials["finnhub_key"]
|
||||
self.finnhub_client = finnhub.Client(api_key=finnhub_key)
|
||||
mongo_user = self.credentials["mongo_db_user"]
|
||||
mongo_password = self.credentials["mongo_db_password"]
|
||||
mongo_url = f"mongodb+srv://{mongo_user}:{mongo_password}@gwendolyn"
|
||||
mongo_url += ".qkwfy.mongodb.net/Gwendolyn?retryWrites=true&w=majority"
|
||||
database_clint = MongoClient(mongo_url)
|
||||
|
||||
if self.options["testing"]:
|
||||
self.log("Testing mode")
|
||||
self.database = database_clint["Gwendolyn-Test"]
|
||||
else:
|
||||
self.database = database_clint["Gwendolyn"]
|
||||
|
||||
def _add_util_classes(self):
|
||||
"""Add all the classes used as utility."""
|
||||
self.database_funcs = DatabaseFuncs(self)
|
||||
self.event_handler = EventHandler(self)
|
||||
self.error_handler = ErrorHandler(self)
|
||||
slash_parameters = {
|
||||
"sync_commands": True,
|
||||
"sync_on_cog_reload": True,
|
||||
"override_type": True
|
||||
}
|
||||
self.slash = discord_slash.SlashCommand(self, **slash_parameters)
|
||||
|
||||
def _add_function_containers(self):
|
||||
"""Add all the function containers used for commands."""
|
||||
self.star_wars = StarWars(self)
|
||||
self.other = Other(self)
|
||||
self.lookup_funcs = LookupFuncs(self)
|
||||
self.games = Games(self)
|
||||
self.money = Money(self)
|
||||
|
||||
def _add_cogs(self):
|
||||
"""Load cogs."""
|
||||
for filename in os.listdir("./gwendolyn/cogs"):
|
||||
if filename.endswith(".py"):
|
||||
self.load_extension(f"gwendolyn.cogs.{filename[:-3]}")
|
||||
|
||||
def log(self, messages, channel: str = "", level: int = 20):
|
||||
"""Log a message. Described in utils/util_functions.py."""
|
||||
log_this(messages, channel, level)
|
||||
|
||||
async def stop(self, ctx: discord_slash.context.SlashContext):
|
||||
"""
|
||||
Stop the bot, and stop running games.
|
||||
|
||||
Only stops the bot if the user in ctx.author is one of the
|
||||
admins given in options.txt.
|
||||
|
||||
*parameters*
|
||||
------------
|
||||
ctx: discord_slash.context.SlashContext
|
||||
The context of the "/stop" slash command.
|
||||
"""
|
||||
if f"#{ctx.author.id}" in self.options["admins"]:
|
||||
await ctx.send("Pulling git repo and restarting...")
|
||||
|
||||
await self.change_presence(status=discord.Status.offline)
|
||||
|
||||
self.database_funcs.wipe_games()
|
||||
if not self.options["testing"]:
|
||||
git_client = git.cmd.Git("")
|
||||
git_client.pull()
|
||||
|
||||
self.log("Logging out", level=25)
|
||||
await self.close()
|
||||
else:
|
||||
log_message = f"{ctx.author.display_name} tried to stop me!"
|
||||
self.log(log_message, str(ctx.channel_id))
|
||||
await ctx.send(f"I don't think I will, {ctx.author.display_name}")
|
||||
|
||||
async def defer(self, ctx: discord_slash.context.SlashContext):
|
||||
"""Send a "Gwendolyn is thinking" message to the user."""
|
||||
try:
|
||||
await ctx.defer()
|
||||
except discord_slash.error.AlreadyResponded:
|
||||
self.log("defer failed")
|
||||
|
@ -140,11 +140,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"connect_four_surrender" : {
|
||||
"base" : "connect_four",
|
||||
"name" : "surrender",
|
||||
"description" : "Surrender the game of connect four"
|
||||
},
|
||||
"downloading" : {
|
||||
"name" : "downloading",
|
||||
"description" : "See current downloads for Plex",
|
||||
@ -187,16 +182,10 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"hangman_start" : {
|
||||
"base" : "hangman",
|
||||
"name" : "start",
|
||||
"hangman" : {
|
||||
"name" : "hangman",
|
||||
"description" : "Start a game of hangman"
|
||||
},
|
||||
"hangman_stop" : {
|
||||
"base" : "hangman",
|
||||
"name" : "stop",
|
||||
"description" : "Stop the current game of hangman"
|
||||
},
|
||||
"hello" : {
|
||||
"name" : "hello",
|
||||
"description" : "Greet Gwendolyn"
|
||||
|
@ -8,4 +8,5 @@ from .helper_classes import DatabaseFuncs
|
||||
from .event_handlers import EventHandler, ErrorHandler
|
||||
from .util_functions import (get_params, log_this, cap, make_files,
|
||||
replace_multiple, emoji_to_command, long_strings,
|
||||
sanitize, get_options, get_credentials)
|
||||
sanitize, get_options, get_credentials, encode_id,
|
||||
decode_id)
|
||||
|
@ -7,16 +7,14 @@ Classes used to handle bot events and errors.
|
||||
ErrorHandler
|
||||
"""
|
||||
import traceback # Used to get the traceback of errors
|
||||
import sys # Used to get traceback when the specific error is not
|
||||
# available
|
||||
|
||||
import discord # Used to init discord.Game and discord.Status, as well
|
||||
# as compare errors to discord errors and as typehints
|
||||
from discord.ext import commands # Used to compare errors with command
|
||||
# errors
|
||||
|
||||
from discord_slash.context import SlashContext
|
||||
from gwendolyn.utils.util_functions import emoji_to_command
|
||||
from discord_slash.context import SlashContext, ComponentContext
|
||||
from gwendolyn.utils.util_functions import decode_id
|
||||
|
||||
|
||||
class EventHandler():
|
||||
@ -63,68 +61,53 @@ class EventHandler():
|
||||
log_message = f"{ctx.author.display_name} ran {full_command}"
|
||||
self.bot.log(log_message, str(ctx.channel_id), level=25)
|
||||
|
||||
async def on_reaction_add(self, reaction: discord.Reaction,
|
||||
user: discord.User):
|
||||
"""Take action if the reaction is on a command message."""
|
||||
if not user.bot:
|
||||
tests = self.bot.database_funcs
|
||||
message = reaction.message
|
||||
channel = message.channel
|
||||
reacted_message = f"{user.display_name} reacted to a message"
|
||||
self.bot.log(reacted_message, str(channel.id))
|
||||
plex_data = tests.plex_reaction_test(message)
|
||||
# plex_data is a list containing 3 elements: whether it was
|
||||
# the add_show/add_movie command message the reaction was to
|
||||
# (bool), whether it's a movie (bool) (if false, it's a
|
||||
# show), and the imdb ids/names for the for the movies or
|
||||
# shows listed in the message (list).
|
||||
async def on_component(self, ctx: ComponentContext):
|
||||
info = decode_id(ctx.custom_id)
|
||||
self.bot.log(f"Component action with info {info}")
|
||||
channel = ctx.origin_message.channel
|
||||
|
||||
reaction_test_parameters = [message, f"#{str(user.id)}"]
|
||||
if info[0].lower() == "plex":
|
||||
if info[1].lower() == "movie":
|
||||
await self.bot.other.plex.add_movie(
|
||||
ctx.origin_message,
|
||||
info[2],
|
||||
not isinstance(channel, discord.DMChannel)
|
||||
)
|
||||
|
||||
if tests.connect_four_reaction_test(*reaction_test_parameters):
|
||||
column = emoji_to_command(reaction.emoji)
|
||||
params = [message, f"#{user.id}", column-1]
|
||||
await self.bot.games.connect_four.place_piece(*params)
|
||||
elif info[1].lower() == "show":
|
||||
await self.bot.other.plex.add_show(
|
||||
ctx.origin_message,
|
||||
info[2],
|
||||
not isinstance(channel, discord.DMChannel)
|
||||
)
|
||||
|
||||
if plex_data[0]:
|
||||
plex_functions = self.bot.other.plex
|
||||
if plex_data[1]:
|
||||
movie_pick = emoji_to_command(reaction.emoji)
|
||||
if movie_pick == "none":
|
||||
imdb_id = None
|
||||
else:
|
||||
imdb_id = plex_data[2][movie_pick-1]
|
||||
elif info[0].lower() == "hangman":
|
||||
if str(ctx.author_id) == info[2]:
|
||||
if info[1].lower() == "guess":
|
||||
await self.bot.games.hangman.guess(ctx, *info[3:])
|
||||
elif info[1].lower() == "end":
|
||||
await self.bot.games.hangman.stop(ctx, *info[3:])
|
||||
|
||||
if isinstance(channel, discord.DMChannel):
|
||||
await message.delete()
|
||||
await plex_functions.add_movie(message, imdb_id, False)
|
||||
else:
|
||||
await message.clear_reactions()
|
||||
await plex_functions.add_movie(message, imdb_id)
|
||||
else:
|
||||
show_pick = emoji_to_command(reaction.emoji)
|
||||
if show_pick == "none":
|
||||
imdb_name = None
|
||||
else:
|
||||
imdb_name = plex_data[2][show_pick-1]
|
||||
elif info[0].lower() == "connectfour":
|
||||
connect_four = self.bot.games.connect_four
|
||||
if info[1].lower() == "place" and str(ctx.author_id) == info[2]:
|
||||
params = [
|
||||
ctx,
|
||||
info[3],
|
||||
int(info[4]),
|
||||
[int(info[5]), int(info[6])],
|
||||
int(info[7]),
|
||||
ctx.author_id,
|
||||
int(info[8])
|
||||
]
|
||||
await connect_four.place_piece(*params)
|
||||
if info[1].lower() == "end":
|
||||
if str(ctx.author_id) in [info[2], info[3]]:
|
||||
params = [
|
||||
ctx, [int(info[2]), int(info[3])], info[4], info[5]
|
||||
]
|
||||
await connect_four.surrender(*params)
|
||||
|
||||
if isinstance(channel, discord.DMChannel):
|
||||
await message.delete()
|
||||
await plex_functions.add_show(message, imdb_name, False)
|
||||
else:
|
||||
await message.clear_reactions()
|
||||
await plex_functions.add_show(message, imdb_name)
|
||||
|
||||
elif tests.hangman_reaction_test(*reaction_test_parameters):
|
||||
self.bot.log("They reacted to the hangman message")
|
||||
if ord(reaction.emoji) in range(127462, 127488):
|
||||
# The range is letter-emojis
|
||||
guess = chr(ord(reaction.emoji)-127397)
|
||||
# Converts emoji to letter
|
||||
params = [message, f"#{user.id}", guess]
|
||||
await self.bot.games.hangman.guess(*params)
|
||||
else:
|
||||
self.bot.log("Bot they didn't react with a valid guess")
|
||||
|
||||
|
||||
class ErrorHandler():
|
||||
@ -166,12 +149,8 @@ class ErrorHandler():
|
||||
|
||||
async def on_error(self, method: str):
|
||||
"""Log when there's an error."""
|
||||
error_type = sys.exc_info()[0]
|
||||
if error_type == discord.errors.NotFound:
|
||||
self.bot.log("Deleted message before I could add all reactions")
|
||||
else:
|
||||
exception = traceback.format_exc()
|
||||
exception = traceback.format_exc()
|
||||
|
||||
exception_string = "".join(exception)
|
||||
log_messages = [f"exception in {method}", f"{exception_string}"]
|
||||
self.bot.log(log_messages, level=40)
|
||||
exception_string = "".join(exception)
|
||||
log_messages = [f"exception in {method}", f"{exception_string}"]
|
||||
self.bot.log(log_messages, level=40)
|
||||
|
@ -6,7 +6,6 @@ Contains classes used for utilities.
|
||||
DatabaseFuncs()
|
||||
"""
|
||||
import os # Used to test if files exist
|
||||
import json # Used to read the data about add_movie/add_show
|
||||
import time # Used to test how long it's been since commands were synced
|
||||
|
||||
import re # Used in get_id
|
||||
@ -25,11 +24,6 @@ class DatabaseFuncs():
|
||||
wipe_games()
|
||||
connect_four_reaction_test(message: discord.Message,
|
||||
user: discord.User) -> bool
|
||||
hangman_reaction_test(message: discord.Message,
|
||||
user: discord.User) -> bool
|
||||
plex_reaction_test(message: discord.Message,
|
||||
user: discord.User) -> bool, bool,
|
||||
list
|
||||
imdb_commands()
|
||||
"""
|
||||
|
||||
@ -112,8 +106,7 @@ class DatabaseFuncs():
|
||||
game_types = [
|
||||
"trivia questions",
|
||||
"blackjack games",
|
||||
"connect 4 games",
|
||||
"hangman games",
|
||||
"connect 4 games"
|
||||
"hex games"
|
||||
]
|
||||
for game_type in game_types:
|
||||
@ -164,87 +157,6 @@ class DatabaseFuncs():
|
||||
|
||||
return valid_reaction
|
||||
|
||||
def hangman_reaction_test(self, message: discord.Message,
|
||||
user: discord.User):
|
||||
"""
|
||||
Test if the given message is the current hangman game.
|
||||
|
||||
Also tests if the given user is the one who's playing hangman.
|
||||
|
||||
*Parameters*
|
||||
------------
|
||||
message: discord.Message
|
||||
The message to test.
|
||||
user: discord.User
|
||||
The user to test.
|
||||
*Returns*
|
||||
---------
|
||||
: bool
|
||||
Whether the given message is the current hangman game
|
||||
and if the user who reacted is the user who's playing
|
||||
hangman.
|
||||
"""
|
||||
channel = message.channel
|
||||
file_path = f"gwendolyn/resources/games/old_images/hangman{channel.id}"
|
||||
if os.path.isfile(file_path):
|
||||
with open(file_path, "r") as file_pointer:
|
||||
old_messages = file_pointer.read().splitlines()
|
||||
else:
|
||||
return False
|
||||
game_message = False
|
||||
|
||||
for old_message in old_messages:
|
||||
old_message_id = int(old_message)
|
||||
if message.id == old_message_id:
|
||||
database = self.bot.database["hangman games"]
|
||||
channel_search = {"_id": str(channel.id)}
|
||||
game = database.find_one(channel_search)
|
||||
if user == game["player"]:
|
||||
game_message = True
|
||||
|
||||
break
|
||||
|
||||
return game_message
|
||||
|
||||
def plex_reaction_test(self, message: discord.Message):
|
||||
"""
|
||||
Test if the given message is the response to a plex request.
|
||||
|
||||
*Parameters*
|
||||
------------
|
||||
message: discord.Message
|
||||
The message to test.
|
||||
|
||||
*Returns*
|
||||
---------
|
||||
: bool
|
||||
Whether the message is the response to a plex request.
|
||||
: bool
|
||||
Whether it was a movie request (false for a show
|
||||
request)
|
||||
: list
|
||||
A list of ids or names of the shows or movies that
|
||||
Gwendolyn presented after the request.
|
||||
"""
|
||||
channel = message.channel
|
||||
old_messages_path = "gwendolyn/resources/plex/"
|
||||
file_path = old_messages_path + f"old_message{str(channel.id)}"
|
||||
if os.path.isfile(file_path):
|
||||
with open(file_path, "r") as file_pointer:
|
||||
data = json.load(file_pointer)
|
||||
else:
|
||||
return (False, None, None)
|
||||
|
||||
if data["message_id"] != message.id:
|
||||
return (False, None, None)
|
||||
|
||||
if "imdb_ids" in data:
|
||||
return_data = (True, True, data["imdb_ids"])
|
||||
else:
|
||||
return_data = (True, False, data["imdb_names"])
|
||||
|
||||
return return_data
|
||||
|
||||
async def imdb_commands(self):
|
||||
"""Sync the slash commands with the discord API."""
|
||||
collection = self.bot.database["last synced"]
|
||||
|
@ -21,6 +21,17 @@ import logging # Used for logging
|
||||
import os # Used by make_files() to check if files exist
|
||||
import sys # Used to specify printing for logging
|
||||
import imdb # Used to disable logging for the module
|
||||
import string
|
||||
|
||||
|
||||
BASE_37 = ":" + string.digits + string.ascii_uppercase
|
||||
BASE_128 = list(
|
||||
string.digits +
|
||||
string.ascii_letters +
|
||||
"!#$€£¢¥¤&%()*+,-./;:<=>?@[]_{|}~ `¦§©®«»±µ·¿əʒ" +
|
||||
"ÆØÅÐÉÈÊÇÑÖ" +
|
||||
"æøåðéèêçñö"
|
||||
)
|
||||
|
||||
# All of this is logging configuration
|
||||
FORMAT = " %(asctime)s | %(name)-16s | %(levelname)-8s | %(message)s"
|
||||
@ -340,3 +351,39 @@ def emoji_to_command(emoji: str):
|
||||
return_value = ""
|
||||
|
||||
return return_value
|
||||
|
||||
def encode_id(info: list):
|
||||
letters = list(":".join(info))
|
||||
dec = 0
|
||||
for i, letter in enumerate(letters):
|
||||
try:
|
||||
dec += (37**i) * BASE_37.index(letter.upper())
|
||||
except ValueError:
|
||||
log_this(f"Could not encode letter {letter}", level=30)
|
||||
|
||||
custom_id = []
|
||||
|
||||
while dec:
|
||||
custom_id.append(BASE_128[dec % 128])
|
||||
dec = dec // 128
|
||||
|
||||
custom_id = ''.join(custom_id)
|
||||
log_this(f"Encoded {info} to {custom_id}")
|
||||
return custom_id
|
||||
|
||||
|
||||
def decode_id(custom_id: str):
|
||||
letters = list(custom_id)
|
||||
dec = 0
|
||||
for i, letter in enumerate(letters):
|
||||
dec += (128**i) * BASE_128.index(letter)
|
||||
|
||||
info_string = []
|
||||
|
||||
while dec:
|
||||
info_string.append(BASE_37[dec % 37])
|
||||
dec = dec // 37
|
||||
|
||||
info = ''.join(info_string).split(':')
|
||||
log_this(f"Decoded {custom_id} to be {info}")
|
||||
return ''.join(info_string).split(':')
|
||||
|
@ -23,7 +23,6 @@ lxml==4.6.3
|
||||
more-itertools==8.8.0
|
||||
multidict==5.1.0
|
||||
Pillow==8.3.1
|
||||
pip==21.2.3
|
||||
pymongo==3.12.0
|
||||
requests==2.26.0
|
||||
setuptools==57.4.0
|
||||
|
Reference in New Issue
Block a user