diff --git a/gwendolyn/cogs/lookup_cog.py b/gwendolyn/cogs/lookup_cog.py index 77c1114..fa5de7f 100644 --- a/gwendolyn/cogs/lookup_cog.py +++ b/gwendolyn/cogs/lookup_cog.py @@ -18,13 +18,13 @@ class LookupCog(commands.Cog): @cog_ext.cog_slash(**params["spell"]) async def spell(self, ctx, query): """Look up a spell.""" - await self.bot.lookup_funcs.spellFunc(ctx, query) + await self.bot.lookup_funcs.spell_func(ctx, query) # Looks up a monster @cog_ext.cog_slash(**params["monster"]) async def monster(self, ctx, query): """Look up a monster.""" - await self.bot.lookup_funcs.monsterFunc(ctx, query) + await self.bot.lookup_funcs.monster_func(ctx, query) def setup(bot): diff --git a/gwendolyn/exceptions.py b/gwendolyn/exceptions.py index 25ced8f..c7340ee 100644 --- a/gwendolyn/exceptions.py +++ b/gwendolyn/exceptions.py @@ -1,3 +1,5 @@ +"""Exceptions for Gwendolyn""" + class GameNotInDatabase(Exception): def __init__(self, game: str, channel: str): self.message = f"There is no {game} game in channel {channel}" diff --git a/gwendolyn/funcs/games/blackjack.py b/gwendolyn/funcs/games/blackjack.py index e3c307d..1a22ce9 100644 --- a/gwendolyn/funcs/games/blackjack.py +++ b/gwendolyn/funcs/games/blackjack.py @@ -65,7 +65,7 @@ class Blackjack(CardGame): def __init__(self, bot): """Initialize the class.""" - super().__init__(bot, "blackjack", DrawBlackjack(bot, self), 4) + super().__init__(bot, "blackjack", DrawBlackjack, 4) default_buttons = ["Hit", "Stand", "Double", "Split"] self.default_buttons = [(i, [i, "0"], 1) for i in default_buttons] @@ -295,7 +295,7 @@ class Blackjack(CardGame): return user_hands, all_standing, pre_all_standing - def _blackjack_finish(self, channel: Messageable): + async def _blackjack_finish(self, channel: Messageable): """ Generate the winnings message after the blackjack game ends. @@ -442,7 +442,7 @@ class Blackjack(CardGame): self.bot.log(log_message, str(channel.id)) await self._blackjack_loop(channel, game_round+1, game_id) else: - await channel.send(self._blackjack_finish(channel)) + await channel.send(await self._blackjack_finish(channel)) async def _get_hand_number(self, ctx: IntCont, command: str, hands_amount: int): @@ -776,7 +776,7 @@ class Blackjack(CardGame): if len(game["user hands"]) == 0: await ctx.channel.send("No one entered the game. Ending the game.") - await ctx.channel.send(self._blackjack_finish(ctx.channel)) + await ctx.channel.send(await self._blackjack_finish(ctx.channel)) return game_id = game["game_id"] diff --git a/gwendolyn/funcs/games/connect_four.py b/gwendolyn/funcs/games/connect_four.py index 40c9eb1..6aac84d 100644 --- a/gwendolyn/funcs/games/connect_four.py +++ b/gwendolyn/funcs/games/connect_four.py @@ -75,7 +75,7 @@ class ConnectFour(BoardGame): def __init__(self, bot): """Initialize the class.""" - super().__init__(bot, "connectfour", DrawConnectFour(bot)) + super().__init__(bot, "connectfour", DrawConnectFour) self.get_name = self.bot.database_funcs.get_name # pylint: disable=invalid-name self.AISCORES = { @@ -108,7 +108,7 @@ class ConnectFour(BoardGame): await self.bot.defer(ctx) channel = str(ctx.channel_id) - opponent_info = self._test_opponent(ctx, opponent) + opponent_info = await self._test_opponent(ctx, opponent) if not opponent_info: return @@ -710,7 +710,7 @@ class DrawConnectFour(): White, but with the alpha set to win_bar_alpha. """ - def __init__(self, bot): + def __init__(self, bot, game): """Initialize the class.""" self.bot = bot self.get_name = self.bot.database_funcs.get_name diff --git a/gwendolyn/funcs/games/game_base.py b/gwendolyn/funcs/games/game_base.py index b84e0ce..6dd6168 100644 --- a/gwendolyn/funcs/games/game_base.py +++ b/gwendolyn/funcs/games/game_base.py @@ -22,7 +22,7 @@ class GameBase(): self.long_strings = self.bot.long_strings self.resources = "gwendolyn/resources/games/" self.game_name = game_name - self.draw = drawer + self.draw = drawer(bot, self) def _get_action_rows(self, buttons: list[tuple[str, list]]): self.bot.log("Generation action rows") @@ -100,7 +100,7 @@ class DatabaseGame(GameBase): async def _send_image(self, channel: Messageable, buttons: list[tuple[str, list]] = None, delete=True): - old_image = super()._send_image(channel, buttons, delete) + old_image = await super()._send_image(channel, buttons, delete) with open(self.old_images_path + str(channel.id), "w") as file_pointer: file_pointer.write(str(old_image.id)) @@ -153,7 +153,7 @@ class CardGame(DatabaseGame): return drawn_card class BoardGame(GameBase): - def _test_opponent(self, ctx: IntCont, opponent: Union[int, User]): + async def _test_opponent(self, ctx: IntCont, opponent: Union[int, User]): if isinstance(opponent, int): # Opponent is Gwendolyn if opponent in range(1, 6): diff --git a/gwendolyn/funcs/lookup/__init__.py b/gwendolyn/funcs/lookup/__init__.py index 2329a0a..cdbeea9 100644 --- a/gwendolyn/funcs/lookup/__init__.py +++ b/gwendolyn/funcs/lookup/__init__.py @@ -2,4 +2,4 @@ __all__ = ["LookupFuncs"] -from .lookup_funcs import LookupFuncs \ No newline at end of file +from .lookup_funcs import LookupFuncs diff --git a/gwendolyn/funcs/lookup/lookup_funcs.py b/gwendolyn/funcs/lookup/lookup_funcs.py index 19f6a44..d2a54f7 100644 --- a/gwendolyn/funcs/lookup/lookup_funcs.py +++ b/gwendolyn/funcs/lookup/lookup_funcs.py @@ -4,22 +4,162 @@ import discord from gwendolyn.utils import cap +STATS = [ + "strength", + "dexterity", + "constitution", + "intelligence", + "wisdom", + "charisma" +] + +def mod(statistic): + """Calculates D&D modifier.""" + modifier = math.floor((statistic-10)/2) + if modifier >= 0: + modifier = "+"+str(modifier) + + return modifier class LookupFuncs(): def __init__(self, bot): self.bot = bot - self.saves = ["strength_save","dexterity_save","constitution_save","intelligence_save","wisdom_save","charisma_save"] - self.abilities = ["acrobatics","animal_handling","arcana","athletics","deception","history","insight","intimidation","investigation","medicine","nature","perception","performance","persuasion","religion","sleight_of_hand","stealth","survival"] + self.saves = [ + "strength_save", + "dexterity_save", + "constitution_save", + "intelligence_save", + "wisdom_save", + "charisma_save" + ] + self.abilities = [ + "acrobatics", + "animal_handling", + "arcana", + "athletics", + "deception", + "history", + "insight", + "intimidation", + "investigation", + "medicine", + "nature", + "perception", + "performance", + "persuasion", + "religion", + "sleight_of_hand", + "stealth", + "survival" + ] - # Calculates D&D stat modifier - def modifier(self, statistic): - mods = math.floor((statistic-10)/2) - if mods >= 0: - mods = "+"+str(mods) - return(str(mods)) + def _format_monster(self, monster): + # Looks at the information about the monster and + # returns that information in separate variables, + # allowing Gwendolyn to know where to separate + # the messages + types = monster["type"] + if monster["subtype"] != "": + types += " ("+monster["subtype"]+")" + + stats = [] + for stat in STATS: + value = monster[stat] + stats.append(f"**{cap(stat[:3])}:** {value} ({mod(value)})") + + stats = "\t".join(stats[:3]) + "\n" + "\t".join(stats[3:]) + + saving_throws = [] + for save in self.saves: + if save in monster: + value = monster[save] + if monster[save] >= 0: + saving_throws.append(f"{cap(save[:3])} +{value}") + else: + saving_throws.append(f"{cap(save[:3])} {value}") + + if saving_throws: + saving_throws = f"\n**Saving Throws:** {', '.join(saving_throws)}" + else: + saving_throws = "" + + skills = [] + for skill in self.abilities: + if skill in monster: + skill_name = cap(skill.replace("_"," ")) + if monster[skill] >= 0: + skills.append(f"{skill_name} +{monster[skill]}") + else: + skills.append(f"{skill_name} {monster[skill]}") + + if skills: + skills = f"\n**Skills:** {', '.join(skills)}" + else: + skills = "" + + vulnerabilities = monster["damage_vulnerabilities"] + if vulnerabilities != "": + vulnerabilities = "\n**Damage Vulnerabilities** "+vulnerabilities + + resistances = monster["damage_resistances"] + if resistances != "": + resistances = "\n**Damage Resistances** "+resistances + + immunities = monster["damage_immunities"] + if immunities != "": + immunities = "\n**Damage Immunities** "+immunities + + c_immunities = monster["condition_immunities"] + if c_immunities != "": + c_immunities = "\n**Condition Immunities** "+c_immunities + + special_abilities = "" + if "special_abilities" in monster: + for ability in monster["special_abilities"]: + special_abilities += "\n\n***"+ability["name"]+".*** "+ability["desc"] + + act = "" + if "actions" in monster: + for action in monster["actions"]: + act += "\n\n***"+action["name"]+".*** "+action["desc"] + + react = "" + if "reactions" in monster: + for reaction in monster["reactions"]: + react += "\n\n***"+reaction["name"]+".*** "+reaction["desc"] + + legendaryActions = "" + if "legendary_actions" in monster: + for action in monster["legendary_actions"]: + legendaryActions += "\n\n***"+action["name"]+".*** "+action["desc"] + + hit_dice = monster["hit_dice"] + dice_amount = int(monster["hit_dice"].replace("d"," ").split()[0]) + con_mod = math.floor((monster['constitution']-10)/2) + if con_mod < 0: + hit_dice += f" - {abs(con_mod) * dice_amount}" + elif con_mod > 0: + hit_dice += (f" + {con_mod * dice_amount}") + + new_part = "\n--------------------" + + monster_type = f"*{monster['size']} {types}, {monster['alignment']}*" + + basic_info = "\n**Armor Class** "+str(monster["armor_class"])+"\n**Hit Points** "+str(monster["hit_points"])+" ("+hit_dice+")\n**Speed **"+monster["speed"]+new_part+"\n" + + info = (monster_type+new_part+basic_info+stats+new_part+saving_throws+skills+vulnerabilities+resistances+immunities+c_immunities+"\n**Senses** "+monster["senses"]+"\n**Languages** "+monster["languages"]+"\n**Challenge** "+monster["challenge_rating"]) + + monster_info = [(info, monster['name']), + (special_abilities, "Special Abilities"), + (act, "Actions"), + (react, "Reactions"), + (legendaryActions, "Legendary Actions")] + + self.bot.log("Returning monster information") + return monster_info # Looks up a monster - async def monsterFunc(self, ctx, query): + async def monster_func(self, ctx, query): query = cap(query) self.bot.log("Looking up "+query) @@ -27,124 +167,41 @@ class LookupFuncs(): if len(query) < 2: self.bot.log("Monster name too short") await ctx.send("I don't know that monster...") + return + + # Opens "monsters.json" + monster_file_path = "gwendolyn/resources/lookup/monsters.json" + with open(monster_file_path,"r", encoding="utf-8") as file_pointer: + data = json.load(file_pointer) + + for monster in data: + if "name" in monster and str(query) == monster["name"]: + self.bot.log("Found it!") + + monster_info = self._format_monster(monster) + + # Sends the received information. Separates into separate messages if + # there is too much text + await ctx.send(f"Result for \"{query}\"") + for text, title in monster_info: + if text != "": + if len(text) < 2000: + em = discord.Embed(title = title, description = text, colour=0xDEADBF) + await ctx.channel.send(embed = em) + else: + index = text[:2000].rfind(".")+1 + em1 = discord.Embed(title = title, description = text[:index], colour=0xDEADBF) + await ctx.channel.send(embed = em1) + em2 = discord.Embed(title = "", description = text[index+1:], colour=0xDEADBF) + await ctx.channel.send(embed = em2) + + break else: - # Opens "monsters.json" - data = json.load(open('gwendolyn/resources/lookup/monsters.json', encoding = "utf8")) - for monster in data: - if "name" in monster and str(query) == monster["name"]: - self.bot.log("Found it!") - - # Looks at the information about the monster and returns that information - # in separate variables, allowing Gwendolyn to know where to separate - # the messages - if monster["subtype"] != "": - types = (monster["type"]+" ("+monster["subtype"]+")") - else: - types = monster["type"] - con_mod = math.floor((monster["constitution"]-10)/2) - hit_dice = monster["hit_dice"] - - stats = ("**Str:** "+str(monster["strength"])+" ("+self.modifier(monster["strength"])+")\t**Dex:** "+str(monster["dexterity"])+" ("+self.modifier(monster["dexterity"])+")\t**Con:** "+str(monster["constitution"])+" ("+self.modifier(monster["constitution"])+")\n**Int: **"+str(monster["intelligence"])+" ("+self.modifier(monster["intelligence"])+")\t**Wis: **"+str(monster["wisdom"])+" ("+self.modifier(monster["wisdom"])+")\t**Cha: **"+str(monster["charisma"])+" ("+self.modifier(monster["charisma"])+")") - - saving_throws = "" - for save in self.saves: - if save in monster: - if monster[save] >= 0: - saving_throws += " "+cap(save[:3])+" +"+str(monster[save])+"," - else: - saving_throws += " "+cap(save[:3])+" "+str(monster[save])+"," - if saving_throws != "": - saving_throws = "\n**Saving Throws**"+saving_throws[:-1] - - skills = "" - for skill in self.abilities: - if skill in monster: - if monster[skill] >= 0: - skills += " "+cap(skill.replace("_"," "))+" +"+str(monster[skill])+"," - else: - skills += " "+cap(skill.replace("_"," "))+" "+str(monster[skill])+"," - if skills != "": - skills = "\n**Skills**"+skills[:-1] - - vulnerabilities = monster["damage_vulnerabilities"] - if vulnerabilities != "": - vulnerabilities = "\n**Damage Vulnerabilities** "+vulnerabilities - - resistances = monster["damage_resistances"] - if resistances != "": - resistances = "\n**Damage Resistances** "+resistances - - immunities = monster["damage_immunities"] - if immunities != "": - immunities = "\n**Damage Immunities** "+immunities - - c_immunities = monster["condition_immunities"] - if c_immunities != "": - c_immunities = "\n**Condition Immunities** "+c_immunities - - specialAbilities = "" - if "special_abilities" in monster: - for ability in monster["special_abilities"]: - specialAbilities += "\n\n***"+ability["name"]+".*** "+ability["desc"] - - act = "" - if "actions" in monster: - for action in monster["actions"]: - act += "\n\n***"+action["name"]+".*** "+action["desc"] - - react = "" - if "reactions" in monster: - for reaction in monster["reactions"]: - react += "\n\n***"+reaction["name"]+".*** "+reaction["desc"] - - legendaryActions = "" - if "legendary_actions" in monster: - for action in monster["legendary_actions"]: - legendaryActions += "\n\n***"+action["name"]+".*** "+action["desc"] - - if con_mod < 0: - hit_dice += (" - "+str(con_mod * int(monster["hit_dice"].replace("d"," ").split()[0])*(-1))) - if con_mod > 0: - hit_dice += (" + "+str(con_mod * int(monster["hit_dice"].replace("d"," ").split()[0]))) - - new_part = "\n--------------------" - - monster_type = monster["size"]+" "+types+", "+monster["alignment"]+"*" - - basic_info = "\n**Armor Class** "+str(monster["armor_class"])+"\n**Hit Points** "+str(monster["hit_points"])+" ("+hit_dice+")\n**Speed **"+monster["speed"]+new_part+"\n" - - info = (monster_type+new_part+basic_info+stats+new_part+saving_throws+skills+vulnerabilities+resistances+immunities+c_immunities+"\n**Senses** "+monster["senses"]+"\n**Languages** "+monster["languages"]+"\n**Challenge** "+monster["challenge_rating"]) - - monsterInfo = [(info, query), - (specialAbilities, "Special Abilities"), - (act, "Actions"), - (react, "Reactions"), - (legendaryActions, "Legendary Actions")] - - self.bot.log("Returning monster information") - - # Sends the received information. Separates into separate messages if - # there is too much text - await ctx.send(f"Result for \"{query}\"") - for text, title in monsterInfo: - if text != "": - if len(text) < 2000: - em = discord.Embed(title = title, description = text, colour=0xDEADBF) - await ctx.channel.send(embed = em) - else: - index = text[:2000].rfind(".")+1 - em1 = discord.Embed(title = title, description = text[:index], colour=0xDEADBF) - await ctx.channel.send(embed = em1) - em2 = discord.Embed(title = "", description = text[index+1:], colour=0xDEADBF) - await ctx.channel.send(embed = em2) - - break - else: - self.bot.log("Monster not in database") - await ctx.send("I don't know that monster...") + self.bot.log("Monster not in database") + await ctx.send("I don't know that monster...") # Looks up a spell - async def spellFunc(self, ctx, query): + async def spell_func(self, ctx, query): query = cap(query) self.bot.log("Looking up "+query) diff --git a/gwendolyn/utils/event_handlers.py b/gwendolyn/utils/event_handlers.py index e657681..695005a 100644 --- a/gwendolyn/utils/event_handlers.py +++ b/gwendolyn/utils/event_handlers.py @@ -63,6 +63,7 @@ class EventHandler(): self.bot.log(log_message, str(ctx.channel_id), level=25) async def on_component(self, ctx: ComponentContext): + """Handle component interaction.""" info = decode_id(ctx.custom_id) self.bot.log(f"Component action with info {info}") channel = ctx.channel @@ -77,23 +78,22 @@ class EventHandler(): ) return - if info[1].lower() == "show": + elif info[1].lower() == "show": await self.bot.other.plex.add_show( ctx.origin_message, info[2], not isinstance(channel, discord.DMChannel) ) - return + else: + raise InvalidInteraction(ctx.custom_id, info) elif info[0].lower() == "hangman" and author == info[2]: if info[1].lower() == "guess": await self.bot.games.hangman.guess(ctx, *info[3:]) - return - - if info[1].lower() == "end": + elif info[1].lower() == "end": await self.bot.games.hangman.stop(ctx, *info[3:]) - return - + else: + raise InvalidInteraction(ctx.custom_id, info) elif info[0].lower() == "connectfour": connect_four = self.bot.games.connect_four if info[1].lower() == "place" and author == info[2]: @@ -106,19 +106,16 @@ class EventHandler(): ctx.author_id, int(info[8]) ) - return - - if info[1].lower() == "end" and author in [info[2], info[3]]: + elif info[1].lower() == "end" and author in [info[2], info[3]]: await connect_four.surrender( ctx, [int(info[2]), int(info[3])], info[4], info[5] ) - return - + else: + raise InvalidInteraction(ctx.custom_id, info) elif info[0].lower() == "blackjack": await self.bot.games.blackjack.decode_interaction(ctx, info[1:]) - return - - raise InvalidInteraction(ctx.custom_id, info) + else: + raise InvalidInteraction(ctx.custom_id, info) diff --git a/gwendolyn/utils/helper_classes.py b/gwendolyn/utils/helper_classes.py index 4e9333e..ca20c5d 100644 --- a/gwendolyn/utils/helper_classes.py +++ b/gwendolyn/utils/helper_classes.py @@ -139,7 +139,7 @@ class DatabaseFuncs(): old_images_path = "gwendolyn/resources/games/old_images/" file_path = old_images_path + f"connect_four{channel.id}" if os.path.isfile(file_path): - with open(file_path, "r") as file_pointer: + with open(file_path, "r", encoding="utf-8") as file_pointer: old_image = int(file_pointer.read()) else: old_image = 0 diff --git a/gwendolyn/utils/util_functions.py b/gwendolyn/utils/util_functions.py index b2799d6..9689533 100644 --- a/gwendolyn/utils/util_functions.py +++ b/gwendolyn/utils/util_functions.py @@ -109,7 +109,7 @@ def get_options(): options: dict The options of the bot. """ - with open("options.txt", "r") as file_pointer: + with open("options.txt", "r", encoding="utf-8") as file_pointer: data = sanitize(file_pointer.read(), True) options = {} @@ -128,7 +128,7 @@ def get_credentials(): credentials: dict The credentials used by the bot. """ - with open("credentials.txt", "r") as file_pointer: + with open("credentials.txt", "r", encoding="utf-8") as file_pointer: data = sanitize(file_pointer.read()) credentials = {} @@ -155,7 +155,8 @@ def long_strings(): data: dict The long strings and their keys. """ - with open("gwendolyn/resources/long_strings.json", "r") as file_pointer: + long_strings_path = "gwendolyn/resources/long_strings.json" + with open(long_strings_path, "r", encoding="utf-8") as file_pointer: data = json.load(file_pointer) return data @@ -170,7 +171,8 @@ def get_params(): params: dict The parameters for every slash command. """ - with open("gwendolyn/resources/slash_parameters.json", "r") as file_pointer: + path = "gwendolyn/resources/slash_parameters.json" + with open(path, "r", encoding="utf-8") as file_pointer: slash_parameters = json.load(file_pointer) options = get_options() @@ -256,14 +258,14 @@ def make_files(): """Create json file if it doesn't exist.""" if not os.path.isfile(path): log_this(path.split("/")[-1]+" didn't exist. Making it now.") - with open(path, "w") as file_pointer: + with open(path, "w", encoding="utf-8") as file_pointer: json.dump(content, file_pointer, indent=4) def make_txt_file(path, content): """Create txt file if it doesn't exist.""" if not os.path.isfile(path): log_this(path.split("/")[-1]+" didn't exist. Making it now.") - with open(path, "w") as file_pointer: + with open(path, "w", encoding="utf-8") as file_pointer: file_pointer.write(content) def directory(path): @@ -272,7 +274,8 @@ def make_files(): os.makedirs(path) log_this("The "+path.split("/")[-1]+" directory didn't exist") - with open("gwendolyn/resources/starting_files.json") as file_pointer: + file_path = "gwendolyn/resources/starting_files.json" + with open(file_path, "r", encoding="utf-8") as file_pointer: data = json.load(file_pointer) for path, content in data["json"].items():