diff --git a/.gitignore b/.gitignore index aa5010b..affb8ee 100644 --- a/.gitignore +++ b/.gitignore @@ -153,16 +153,16 @@ static token.txt credentials.txt options.txt -resources/starWars/destinyPoints.txt -resources/bedreNetflix/ -resources/games/hilo/ -resources/games/blackjackTables/ -resources/games/oldImages/ -resources/games/connect4Boards/ -resources/games/hexBoards/ -resources/games/hangmanBoards/ -resources/lookup/monsters.json -resources/lookup/spells.json -resources/movies.txt -resources/names.txt +gwendolyn/resources/star_wars/destinyPoints.txt +gwendolyn/resources/plex/ +gwendolyn/resources/games/hilo/ +gwendolyn/resources/games/blackjack_tables/ +gwendolyn/resources/games/old_images/ +gwendolyn/resources/games/connect_four_boards/ +gwendolyn/resources/games/hex_boards/ +gwendolyn/resources/games/hangman_boards/ +gwendolyn/resources/lookup/monsters.json +gwendolyn/resources/lookup/spells.json +gwendolyn/resources/movies.txt +gwendolyn/resources/names.txt gwendolynTest.py diff --git a/Gwendolyn.py b/Gwendolyn.py deleted file mode 100644 index 5a48420..0000000 --- a/Gwendolyn.py +++ /dev/null @@ -1,81 +0,0 @@ -import os, finnhub, platform, asyncio, discord - -from discord.ext import commands -from discord_slash import SlashCommand -from pymongo import MongoClient -from funcs import Money, StarWars, Games, Other, LookupFuncs -from utils import Options, Credentials, logThis, makeFiles, databaseFuncs, EventHandler, ErrorHandler - -class Gwendolyn(commands.Bot): - def __init__(self): - self.options = Options() - self.credentials = Credentials() - self.finnhubClient = finnhub.Client(api_key = self.credentials.finnhubKey) - self.MongoClient = MongoClient(f"mongodb+srv://{self.credentials.mongoDBUser}:{self.credentials.mongoDBPassword}@gwendolyn.qkwfy.mongodb.net/Gwendolyn?retryWrites=true&w=majority") - - if self.options.testing: - self.log("Testing mode") - self.database = self.MongoClient["Gwendolyn-Test"] - else: - self.database = self.MongoClient["Gwendolyn"] - - self.starWars = StarWars(self) - self.other = Other(self) - self.lookupFuncs = LookupFuncs(self) - self.games = Games(self) - self.money = Money(self) - self.databaseFuncs = databaseFuncs(self) - self.eventHandler = EventHandler(self) - self.errorHandler = ErrorHandler(self) - - intents = discord.Intents.default() - intents.members = True - - super().__init__(command_prefix=" ", case_insensitive=True, intents = intents, status = discord.Status.dnd) - - def log(self, messages, channel : str = "", level : int = 20): - logThis(messages, channel, level) - - async def stop(self, ctx): - 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.databaseFuncs.stopServer() - - self.log("Logging out", level = 25) - await self.close() - else: - self.log(f"{ctx.author.display_name} tried to stop me! (error code 201)",str(ctx.channel_id)) - await ctx.send(f"I don't think I will, {ctx.author.display_name} (error code 201)") - - async def defer(self, ctx): - try: - await ctx.defer() - except: - self.log("defer failed") - - - -if __name__ == "__main__": - if platform.system() == "Windows": - asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) - - # Creates the required files - makeFiles() - - # Creates the Bot - bot = Gwendolyn() - bot.slash = SlashCommand(bot, sync_commands=True, sync_on_cog_reload=True, override_type=True) - - #Loads cogs - for filename in os.listdir("./cogs"): - if filename.endswith(".py"): - bot.load_extension(f"cogs.{filename[:-3]}") - - try: - # Runs the whole shabang - bot.run(bot.credentials.token) - except: - bot.log("Could not log in. Remember to write your bot token in the credentials.txt file") \ No newline at end of file diff --git a/cogs/EventCog.py b/cogs/EventCog.py deleted file mode 100644 index 7b72f6b..0000000 --- a/cogs/EventCog.py +++ /dev/null @@ -1,34 +0,0 @@ -from discord.ext import commands - -class EventCog(commands.Cog): - def __init__(self, bot): - self.bot = bot - self.bot.on_error = self.on_error - - # Syncs commands, sets the game, and logs when the bot logs in - @commands.Cog.listener() - async def on_ready(self): - await self.bot.eventHandler.on_ready() - - # Logs when user sends a command - @commands.Cog.listener() - async def on_slash_command(self, ctx): - await self.bot.eventHandler.on_slash_command(ctx) - - # Logs if a command experiences an error - @commands.Cog.listener() - async def on_slash_command_error(self, ctx, error): - await self.bot.errorHandler.on_slash_command_error(ctx, error) - - # Logs if on error occurs - async def on_error(self, method, *args, **kwargs): - await self.bot.errorHandler.on_error(method) - - # If someone reacted to a message, checks if it's a reaction it's - # Gwendolyn has been waiting for, and then does something - @commands.Cog.listener() - async def on_reaction_add(self, reaction, user): - await self.bot.eventHandler.on_reaction_add(reaction, user) - -def setup(bot): - bot.add_cog(EventCog(bot)) diff --git a/cogs/GameCogs.py b/cogs/GameCogs.py deleted file mode 100644 index 4714437..0000000 --- a/cogs/GameCogs.py +++ /dev/null @@ -1,155 +0,0 @@ -from discord.ext import commands -from discord_slash import cog_ext - -from utils import getParams - -params = getParams() - -class GamesCog(commands.Cog): - def __init__(self,bot): - """Runs game stuff.""" - self.bot = bot - - # Checks user balance - @cog_ext.cog_slash(**params["balance"]) - async def balance(self, ctx): - await self.bot.money.sendBalance(ctx) - - # Gives another user an amount of GwendoBucks - @cog_ext.cog_slash(**params["give"]) - async def give(self, ctx, user, amount): - await self.bot.money.giveMoney(ctx, user, amount) - - # Invest GwendoBucks in the stock market - @cog_ext.cog_slash(**params["invest"]) - async def invest(self, ctx, parameters = "check"): - await self.bot.games.invest.parseInvest(ctx, parameters) - - # Runs a game of trivia - @cog_ext.cog_slash(**params["trivia"]) - async def trivia(self, ctx, answer = ""): - await self.bot.games.trivia.triviaParse(ctx, answer) - - -class BlackjackCog(commands.Cog): - def __init__(self,bot): - """Runs game stuff.""" - self.bot = bot - - # Starts a game of blackjack - @cog_ext.cog_subcommand(**params["blackjackStart"]) - async def blackjackStart(self, ctx): - await self.bot.games.blackjack.start(ctx) - - @cog_ext.cog_subcommand(**params["blackjackBet"]) - async def blackjackBet(self, ctx, bet): - await self.bot.games.blackjack.playerDrawHand(ctx, bet) - - @cog_ext.cog_subcommand(**params["blackjackStand"]) - async def blackjackStand(self, ctx, hand = ""): - await self.bot.games.blackjack.stand(ctx, hand) - - @cog_ext.cog_subcommand(**params["blackjackHit"]) - async def blackjackHit(self, ctx, hand = 0): - await self.bot.games.blackjack.hit(ctx, hand) - - @cog_ext.cog_subcommand(**params["blackjackDouble"]) - async def blackjackDouble(self, ctx, hand = 0): - await self.bot.games.blackjack.double(ctx, hand) - - @cog_ext.cog_subcommand(**params["blackjackSplit"]) - async def blackjackSplit(self, ctx, hand = 0): - await self.bot.games.blackjack.split(ctx, hand) - - @cog_ext.cog_subcommand(**params["blackjackHilo"]) - async def blackjackHilo(self, ctx): - await self.bot.games.blackjack.hilo(ctx) - - @cog_ext.cog_subcommand(**params["blackjackShuffle"]) - async def blackjackShuffle(self, ctx): - await self.bot.games.blackjack.shuffle(ctx) - - @cog_ext.cog_subcommand(**params["blackjackCards"]) - async def blackjackCards(self, ctx): - await self.bot.games.blackjack.cards(ctx) - - -class ConnectFourCog(commands.Cog): - def __init__(self,bot): - """Runs game stuff.""" - self.bot = bot - - # Start a game of connect four against a user - @cog_ext.cog_subcommand(**params["connectFourStartUser"]) - async def connectFourStartUser(self, ctx, user): - await self.bot.games.connectFour.start(ctx, user) - - # Start a game of connect four against gwendolyn - @cog_ext.cog_subcommand(**params["connectFourStartGwendolyn"]) - async def connectFourStartGwendolyn(self, ctx, difficulty = 3): - await self.bot.games.connectFour.start(ctx, difficulty) - - # Stop the current game of connect four - @cog_ext.cog_subcommand(**params["connectFourSurrender"]) - async def connectFourSurrender(self, ctx): - await self.bot.games.connectFour.surrender(ctx) - - -class HangmanCog(commands.Cog): - def __init__(self,bot): - """Runs game stuff.""" - self.bot = bot - - # Starts a game of Hangman - @cog_ext.cog_subcommand(**params["hangmanStart"]) - async def hangmanStart(self, ctx): - await self.bot.games.hangman.start(ctx) - - # Stops a game of Hangman - @cog_ext.cog_subcommand(**params["hangmanStop"]) - async def hangmanStop(self, ctx): - await self.bot.games.hangman.stop(ctx) - - -class HexCog(commands.Cog): - def __init__(self,bot): - """Runs game stuff.""" - self.bot = bot - - # Start a game of Hex against another user - @cog_ext.cog_subcommand(**params["hexStartUser"]) - async def hexStartUser(self, ctx, user): - await self.bot.games.hex.start(ctx, user) - - # Start a game of Hex against Gwendolyn - @cog_ext.cog_subcommand(**params["hexStartGwendolyn"]) - async def hexStartGwendolyn(self, ctx, difficulty = 2): - await self.bot.games.hex.start(ctx, difficulty) - - # Place a piece in the hex game - @cog_ext.cog_subcommand(**params["hexPlace"]) - async def hexPlace(self, ctx, coordinates): - await self.bot.games.hex.placeHex(ctx, coordinates, f"#{ctx.author.id}") - - # Undo your last hex move - @cog_ext.cog_subcommand(**params["hexUndo"]) - async def hexUndo(self, ctx): - await self.bot.games.hex.undo(ctx) - - # Perform a hex swap - @cog_ext.cog_subcommand(**params["hexSwap"]) - async def hexSwap(self, ctx): - await self.bot.games.hex.swap(ctx) - - # Surrender the hex game - @cog_ext.cog_subcommand(**params["hexSurrender"]) - async def hexSurrender(self, ctx): - await self.bot.games.hex.surrender(ctx) - - -def setup(bot): - bot.add_cog(GamesCog(bot)) - bot.add_cog(BlackjackCog(bot)) - bot.add_cog(ConnectFourCog(bot)) - bot.add_cog(HangmanCog(bot)) - bot.add_cog(HexCog(bot)) \ No newline at end of file diff --git a/cogs/LookupCog.py b/cogs/LookupCog.py deleted file mode 100644 index 1b23cd1..0000000 --- a/cogs/LookupCog.py +++ /dev/null @@ -1,24 +0,0 @@ -from discord.ext import commands -from discord_slash import cog_ext - -from utils import getParams - -params = getParams() - -class LookupCog(commands.Cog): - def __init__(self, bot): - """Runs lookup commands.""" - self.bot = bot - - # Looks up a spell - @cog_ext.cog_slash(**params["spell"]) - async def spell(self, ctx, query): - await self.bot.lookupFuncs.spellFunc(ctx, query) - - # Looks up a monster - @cog_ext.cog_slash(**params["monster"]) - async def monster(self, ctx, query): - await self.bot.lookupFuncs.monsterFunc(ctx, query) - -def setup(bot): - bot.add_cog(LookupCog(bot)) \ No newline at end of file diff --git a/cogs/MiscCog.py b/cogs/MiscCog.py deleted file mode 100644 index 136c0fb..0000000 --- a/cogs/MiscCog.py +++ /dev/null @@ -1,94 +0,0 @@ -import discord, codecs, string, json -from discord.ext import commands -from discord_slash import cog_ext - -from utils import getParams # pylint: disable=import-error - -params = getParams() - -class MiscCog(commands.Cog): - def __init__(self, bot): - """Runs misc commands.""" - self.bot = bot - self.bot.remove_command("help") - self.generators = bot.other.generators - self.bedreNetflix = bot.other.bedreNetflix - self.nerdShit = bot.other.nerdShit - - # Sends the bot's latency - @cog_ext.cog_slash(**params["ping"]) - async def ping(self, ctx): - await ctx.send(f"Pong!\nLatency is {round(self.bot.latency * 1000)}ms") - - # Restarts the bot - @cog_ext.cog_slash(**params["stop"]) - async def stop(self, ctx): - await self.bot.stop(ctx) - - # Gets help for specific command - @cog_ext.cog_slash(**params["help"]) - async def helpCommand(self, ctx, command = ""): - await self.bot.other.helpFunc(ctx, command) - - # Lets you thank the bot - @cog_ext.cog_slash(**params["thank"]) - async def thank(self, ctx): - await ctx.send("You're welcome :blush:") - - # Sends a friendly message - @cog_ext.cog_slash(**params["hello"]) - async def hello(self, ctx): - await self.bot.other.helloFunc(ctx) - - # Rolls dice - @cog_ext.cog_slash(**params["roll"]) - async def roll(self, ctx, dice = "1d20"): - await self.bot.other.rollDice(ctx, dice) - - # Sends a random image - @cog_ext.cog_slash(**params["image"]) - async def image(self, ctx): - await self.bot.other.imageFunc(ctx) - - # Finds a random movie - @cog_ext.cog_slash(**params["movie"]) - async def movie(self, ctx): - await self.bot.other.movieFunc(ctx) - - # Generates a random name - @cog_ext.cog_slash(**params["name"]) - async def name(self, ctx): - await self.generators.nameGen(ctx) - - # Generates a random tavern name - @cog_ext.cog_slash(**params["tavern"]) - async def tavern(self, ctx): - await self.generators.tavernGen(ctx) - - # Finds a page on the Senkulpa wiki - @cog_ext.cog_slash(**params["wiki"]) - async def wiki(self, ctx, wikiPage = ""): - await self.bot.other.findWikiPage(ctx, wikiPage) - - #Searches for movie and adds it to Bedre Netflix - @cog_ext.cog_slash(**params["addMovie"]) - async def addMovie(self, ctx, movie): - await self.bedreNetflix.requestMovie(ctx, movie) - - #Searches for show and adds it to Bedre Netflix - @cog_ext.cog_slash(**params["addShow"]) - async def addShow(self, ctx, show): - await self.bedreNetflix.requestShow(ctx, show) - - #Returns currently downloading torrents - @cog_ext.cog_slash(**params["downloading"]) - async def downloading(self, ctx, parameters = "-d"): - await self.bedreNetflix.downloading(ctx, parameters) - - #Looks up on Wolfram Alpha - @cog_ext.cog_slash(**params["wolf"]) - async def wolf(self, ctx, query): - await self.nerdShit.wolfSearch(ctx, query) - -def setup(bot): - bot.add_cog(MiscCog(bot)) diff --git a/cogs/StarWarsCog.py b/cogs/StarWarsCog.py deleted file mode 100644 index 57e39f0..0000000 --- a/cogs/StarWarsCog.py +++ /dev/null @@ -1,37 +0,0 @@ -import discord, string, json -from discord.ext import commands -from discord_slash import cog_ext - -from utils import getParams - -params = getParams() - -class starWarsCog(commands.Cog): - - def __init__(self, bot): - """Runs star wars commands.""" - self.bot = bot - - # Rolls star wars dice - @cog_ext.cog_slash(**params["starWarsRoll"]) - async def starWarsRoll(self, ctx, dice = ""): - await self.bot.starWars.roll.parseRoll(ctx, dice) - - # Controls destiny points - @cog_ext.cog_slash(**params["starWarsDestiny"]) - async def starWarsDestiny(self, ctx, parameters = ""): - await self.bot.starWars.destiny.parseDestiny(ctx, parameters) - - # Rolls for critical injuries - @cog_ext.cog_slash(**params["starWarsCrit"]) - async def starWarsCrit(self, ctx, severity : int = 0): - await self.bot.starWars.roll.critRoll(ctx, severity) - - # Accesses and changes character sheet data with the parseChar function - # from funcs/starWarsFuncs/starWarsCharacter.py - @cog_ext.cog_slash(**params["starWarsCharacter"]) - async def starWarsCharacter(self, ctx, parameters = ""): - await self.bot.starWars.character.parseChar(ctx, parameters) - -def setup(bot): - bot.add_cog(starWarsCog(bot)) \ No newline at end of file diff --git a/funcs/games/blackjack.py b/funcs/games/blackjack.py deleted file mode 100644 index 1d68b0d..0000000 --- a/funcs/games/blackjack.py +++ /dev/null @@ -1,966 +0,0 @@ -import random -import math -import datetime -import asyncio -import discord -from PIL import Image, ImageDraw, ImageFont - -from shutil import copyfile - -from utils import replaceMultiple - -class Blackjack(): - def __init__(self,bot): - self.bot = bot - self.draw = DrawBlackjack(bot) - - # Shuffles the blackjack cards - def blackjackShuffle(self, decks, channel): - self.bot.log("Shuffling the blackjack deck") - - with open("resources/games/deckOfCards.txt","r") as f: - deck = f.read() - - allDecks = deck.split("\n") * decks - random.shuffle(allDecks) - - self.bot.database["blackjack cards"].update_one({"_id":channel},{"$set":{"_id":channel,"cards":allDecks}},upsert=True) - - # Creates hilo file - self.bot.log("creating hilo doc for "+channel) - data = 0 - self.bot.database["hilo"].update_one({"_id":channel},{"$set":{"_id":channel,"hilo":data}},upsert=True) - - return - - # Calculates the value of a blackjack hand - def calcHandValue(self, hand : list): - self.bot.log("Calculating hand value") - values = [] - values.append(0) - - for card in hand: - cardValue = card[0] - cardValue = replaceMultiple(cardValue,["0","k","q","j"],"10") - if cardValue == "a": - length = len(values) - for x in range(length): - values.append(values[x] + 11) - values[x] += 1 - else: - for x in range(len(values)): - values[x] += int(cardValue) - - values.sort() - - handValue = values[0] - for value in values: - if value <= 21: - handValue = value - - self.bot.log("Calculated "+str(hand)+" to be "+str(handValue)) - - return handValue - - # Draws a card from the deck - def drawCard(self, channel): - self.bot.log("drawing a card") - - drawnCard = self.bot.database["blackjack cards"].find_one({"_id":channel})["cards"][0] - self.bot.database["blackjack cards"].update_one({"_id":channel},{"$pop":{"cards":-1}}) - value = self.calcHandValue([drawnCard]) - - if value <= 6: - self.bot.database["hilo"].update_one({"_id":channel},{"$inc":{"hilo":1}}) - elif value >= 10: - self.bot.database["hilo"].update_one({"_id":channel},{"$inc":{"hilo":-1}}) - - return drawnCard - - # Dealer draws a card and checks if they should draw another one - def dealerDraw(self,channel): - game = self.bot.database["blackjack games"].find_one({"_id":channel}) - - done = False - dealerHand = game["dealer hand"] - - if self.calcHandValue(dealerHand) < 17: - dealerHand.append(self.drawCard(channel)) - self.bot.database["blackjack games"].update_one({"_id":channel},{"$set":{"dealer hand":dealerHand}}) - else: - done = True - - if self.calcHandValue(dealerHand) > 21: - self.bot.database["blackjack games"].update_one({"_id":channel},{"$set":{"dealer busted":True}}) - - return done - - # Goes to the next round and calculates some stuff - def blackjackContinue(self, channel): - self.bot.log("Continuing blackjack game") - game = self.bot.database["blackjack games"].find_one({"_id":channel}) - - done = False - - self.bot.database["blackjack games"].update_one({"_id":channel},{"$inc":{"round":1}}) - - allStanding = True - preAllStanding = True - message = "All players are standing. The dealer now shows his cards and draws." - - if game["all standing"]: - self.bot.log("All are standing") - - done = self.dealerDraw(channel) - message = "The dealer draws a card." - - game = self.bot.database["blackjack games"].find_one({"_id":channel}) - - self.bot.log("Testing if all are standing") - for user in game["user hands"]: - try: - newUser, allStanding, preAllStanding = self.testIfStanding(game["user hands"][user],allStanding,preAllStanding,True) - self.bot.database["blackjack games"].update_one({"_id":channel},{"$set":{"user hands."+user:newUser}}) - except: - self.bot.log("Error in testing if all are standing (error code 1331)") - - if allStanding: - self.bot.database["blackjack games"].update_one({"_id":channel},{"$set":{"all standing":True}}) - - try: - self.draw.drawImage(channel) - except: - self.bot.log("Error drawing blackjack table (error code 1340)") - - if allStanding: - if done == False: - return message, True, done - else: - return "The dealer is done drawing cards", True, done - elif preAllStanding: - return "", True, done - else: - if game["round"] == 1: - firstRoundMessage = ". You can also double down with \"/blackjack double\" or split with \"/blackjack split\"" - else: - firstRoundMessage = "" - return "You have 2 minutes to either hit or stand with \"/blackjack hit\" or \"/blackjack stand\""+firstRoundMessage+". It's assumed you're standing if you don't make a choice.", False, done - - def testIfStanding(self, hand,allStanding,preAllStanding,topLevel): - if hand["hit"] == False: - hand["standing"] = True - - if hand["standing"] == False: - allStanding = False - - if self.calcHandValue(hand["hand"]) >= 21 or hand["doubled"]: - hand["standing"] = True - else: - preAllStanding = False - - hand["hit"] = False - - if topLevel: - if hand["split"] >= 1: - hand["other hand"], allStanding, preAllStanding = self.testIfStanding(hand["other hand"],allStanding,preAllStanding,False) - if hand["split"] >= 2: - hand["third hand"], allStanding, preAllStanding = self.testIfStanding(hand["third hand"],allStanding,preAllStanding,False) - if hand["split"] >= 3: - hand["fourth hand"], allStanding, preAllStanding = self.testIfStanding(hand["fourth hand"],allStanding,preAllStanding,False) - - return hand, allStanding, preAllStanding - - # When players try to hit - async def hit(self, ctx, handNumber = 0): - await self.bot.defer(ctx) - channel = str(ctx.channel_id) - user = f"#{ctx.author.id}" - roundDone = False - - game = self.bot.database["blackjack games"].find_one({"_id":channel}) - - if user in game["user hands"]: - - userHands = game["user hands"][user] - hand, handNumber = self.getHandNumber(userHands, handNumber) - - if hand == None: - logMessage = "They didn't specify a hand" - sendMessage = "You need to specify a hand" - elif game["round"] <= 0: - logMessage = "They tried to hit on the 0th round" - sendMessage = "You can't hit before you see your cards" - elif hand["hit"]: - logMessage = "They've already hit this round" - sendMessage = "You've already hit this round" - elif hand["standing"]: - logMessage = "They're already standing" - sendMessage = "You can't hit when you're standing" - else: - hand["hand"].append(self.drawCard(channel)) - hand["hit"] = True - - handValue = self.calcHandValue(hand["hand"]) - - if handValue > 21: - hand["busted"] = True - - if handNumber == 2: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user+".other hand":hand}}) - elif handNumber == 3: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user+".third hand":hand}}) - elif handNumber == 4: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user+".fourth hand":hand}}) - else: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user:hand}}) - - roundDone = self.isRoundDone(self.bot.database["blackjack games"].find_one({"_id":channel})) - - sendMessage = f"{ctx.author.display_name} hit" - logMessage = "They succeeded" - else: - logMessage = "They tried to hit without being in the game" - sendMessage = "You have to enter the game before you can hit" - - await ctx.send(sendMessage) - self.bot.log(logMessage) - - if roundDone: - gameID = game["gameID"] - self.bot.log("Hit calling self.blackjackLoop()", channel) - await self.blackjackLoop(ctx.channel, game["round"]+1, gameID) - - # When players try to double down - async def double(self, ctx, handNumber = 0): - await self.bot.defer(ctx) - channel = str(ctx.channel_id) - user = f"#{ctx.author.id}" - roundDone = False - - game = self.bot.database["blackjack games"].find_one({"_id":channel}) - - if user in game["user hands"]: - hand, handNumber = self.getHandNumber(game["user hands"][user],handNumber) - - if hand == None: - logMessage = "They didn't specify a hand" - sendMessage = "You need to specify a hand" - elif game["round"] <= 0: - logMessage = "They tried to hit on the 0th round" - sendMessage = "You can't hit before you see your cards" - elif hand["hit"]: - logMessage = "They've already hit this round" - sendMessage = "You've already hit this round" - elif hand["standing"]: - logMessage = "They're already standing" - sendMessage = "You can't hit when you're standing" - elif len(hand["hand"]) != 2: - logMessage = "They tried to double after round 1" - sendMessage = "You can only double on the first round" - else: - bet = hand["bet"] - if self.bot.money.checkBalance(user) < bet: - logMessage = "They tried to double without being in the game" - sendMessage = "You can't double when you're not in the game" - else: - self.bot.money.addMoney(user,-1 * bet) - - hand["hand"].append(self.drawCard(channel)) - hand["hit"] = True - hand["doubled"] = True - hand["bet"] += bet - - handValue = self.calcHandValue(hand["hand"]) - - - if handValue > 21: - hand["busted"] = True - - if handNumber == 2: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user+".other hand":hand}}) - elif handNumber == 3: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user+".third hand":hand}}) - elif handNumber == 4: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user+".fourth hand":hand}}) - else: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user:hand}}) - - roundDone = self.isRoundDone(self.bot.database["blackjack games"].find_one({"_id":channel})) - - sendMessage = f"Adding another {bet} GwendoBucks to {self.bot.databaseFuncs.getName(user)}'s bet and drawing another card." - logMessage = "They succeeded" - else: - logMessage = "They tried to double without being in the game" - sendMessage = "You can't double when you're not in the game" - - await ctx.send(sendMessage) - self.bot.log(logMessage) - - if roundDone: - gameID = game["gameID"] - self.bot.log("Double calling self.blackjackLoop()", channel) - await self.blackjackLoop(ctx.channel, game["round"]+1, gameID) - - # When players try to stand - async def stand(self, ctx, handNumber = 0): - await self.bot.defer(ctx) - channel = str(ctx.channel_id) - user = f"#{ctx.author.id}" - roundDone = False - - game = self.bot.database["blackjack games"].find_one({"_id":channel}) - - if user in game["user hands"]: - - hand, handNumber = self.getHandNumber(game["user hands"][user],handNumber) - - if hand == None: - sendMessage = "You need to specify which hand" - logMessage = "They didn't specify a hand" - elif game["round"] <= 0: - sendMessage = "You can't stand before you see your cards" - logMessage = "They tried to stand on round 0" - elif hand["hit"]: - sendMessage = "You've already hit this round" - logMessage = "They'd already hit this round" - elif hand["standing"]: - sendMessage = "You're already standing" - logMessage = "They're already standing" - else: - hand["standing"] = True - - if handNumber == 2: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user+".other hand":hand}}) - elif handNumber == 3: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user+".third hand":hand}}) - elif handNumber == 4: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user+".fourth hand":hand}}) - else: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user:hand}}) - - roundDone = self.isRoundDone(self.bot.database["blackjack games"].find_one({"_id":channel})) - - sendMessage = f"{ctx.author.display_name} is standing" - logMessage = "They succeeded" - - else: - logMessage = "They tried to stand without being in the game" - sendMessage = "You have to enter the game before you can stand" - - await ctx.send(sendMessage) - self.bot.log(logMessage) - - if roundDone: - gameID = game["gameID"] - self.bot.log("Stand calling self.blackjackLoop()", channel) - await self.blackjackLoop(ctx.channel, game["round"]+1, gameID) - - # When players try to split - async def split(self, ctx, handNumber = 0): - await self.bot.defer(ctx) - channel = str(ctx.channel_id) - user = f"#{ctx.author.id}" - roundDone = False - handNumberError = False - - game = self.bot.database["blackjack games"].find_one({"_id":channel}) - - if game["user hands"][user]["split"] == 0: - hand = game["user hands"][user] - newHand = game["user hands"][user]["other hand"] - handNumber = 0 - otherHand = 2 - else: - if handNumber == 1: - hand = game["user hands"][user] - elif handNumber == 2: - hand = game["user hands"][user]["other hand"] - elif handNumber == 3: - hand = game["user hands"][user]["third hand"] - else: - handNumberError = True - - if game["user hands"][user]["split"] == 1: - newHand = game["user hands"][user]["third hand"] - otherHand = 3 - else: - newHand = game["user hands"][user]["fourth hand"] - otherHand = 4 - - if handNumberError: - logMessage = "They didn't specify a hand" - sendMessage = "You have to specify the hand you're hitting with" - elif game["round"] == 0: - logMessage = "They tried to split on round 0" - sendMessage = "You can't split before you see your cards" - elif game["user hands"][user]["split"] > 3: - logMessage = "They tried to split more than three times" - sendMessage = "You can only split 3 times" - elif hand["hit"]: - logMessage = "They've already hit" - sendMessage = "You've already hit" - elif hand["standing"]: - logMessage = "They're already standing" - sendMessage = "You're already standing" - elif len(hand["hand"]) != 2: - logMessage = "They tried to split after the first round" - sendMessage = "You can only split on the first round" - else: - firstCard = self.calcHandValue([hand["hand"][0]]) - secondCard = self.calcHandValue([hand["hand"][1]]) - if firstCard != secondCard: - logMessage = "They tried to split two different cards" - sendMessage = "You can only split if your cards have the same value" - else: - bet = hand["bet"] - if self.bot.money.checkBalance(user) < bet: - logMessage = "They didn't have enough GwendoBucks" - sendMessage = "You don't have enough GwendoBucks" - else: - self.bot.money.addMoney(user,-1 * bet) - - hand["hit"] = True - newHand["hit"] = True - - newHand = { - "hand":[],"bet":0,"standing":False,"busted":False, - "blackjack":False,"hit":True,"doubled":False} - - newHand["bet"] = hand["bet"] - - newHand["hand"].append(hand["hand"].pop(1)) - newHand["hand"].append(self.drawCard(channel)) - hand["hand"].append(self.drawCard(channel)) - - handValue = self.calcHandValue(hand["hand"]) - otherHandValue = self.calcHandValue(newHand["hand"]) - if handValue > 21: - hand["busted"] = True - elif handValue == 21: - hand["blackjack"] = True - - if otherHandValue > 21: - newHand["busted"] = True - elif otherHandValue == 21: - newHand["blackjack"] = True - - if handNumber == 2: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user+".other hand":hand}}) - elif handNumber == 3: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user+".third hand":hand}}) - else: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user:hand}}) - - if otherHand == 3: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user+".third hand":newHand}}) - elif otherHand == 4: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user+".fourth hand":newHand}}) - else: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user+".other hand":newHand}}) - - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$inc":{"user hands."+user+".split":1}}) - - roundDone = self.isRoundDone(self.bot.database["blackjack games"].find_one({"_id":channel})) - - sendMessage = f"Splitting {self.bot.databaseFuncs.getName(user)}'s hand into 2. Adding their original bet to the second hand. You can use \"/blackjack hit/stand/double 1\" and \"/blackjack hit/stand/double 2\" to play the different hands." - logMessage = "They succeeded" - - await ctx.send(sendMessage) - self.bot.log(logMessage) - - if roundDone: - gameID = game["gameID"] - self.bot.log("Stand calling self.blackjackLoop()", channel) - await self.blackjackLoop(ctx.channel, game["round"]+1, gameID) - - # Player enters the game and draws a hand - async def playerDrawHand(self, ctx, bet : int): - await self.bot.defer(ctx) - channel = str(ctx.channel_id) - user = f"#{ctx.author.id}" - collection = self.bot.database["blackjack games"] - game = collection.find_one({"_id":channel}) - userName = self.bot.databaseFuncs.getName(user) - - self.bot.log(f"{userName} is trying to join the Blackjack game") - - if game == None: - sendMessage = "There is no game going on in this channel" - logMessage = sendMessage - elif user in game["user hands"]: - sendMessage = "You're already in the game!" - logMessage = "They're already in the game" - elif len(game["user hands"]) >= 5: - sendMessage = "There can't be more than 5 players in a game" - logMessage = "There were already 5 players in the game" - elif game["round"] != 0: - sendMessage = "The table is no longer taking bets" - logMessage = "They tried to join after the game begun" - elif bet < 0: - sendMessage = "You can't bet a negative amount" - logMessage = "They tried to bet a negative amount" - elif self.bot.money.checkBalance(user) < bet: - sendMessage = "You don't have enough GwendoBucks" - logMessage = "They didn't have enough GwendoBucks" - else: - self.bot.money.addMoney(user,-1 * bet) - playerHand = [self.drawCard(channel) for _ in range(2)] - - handValue = self.calcHandValue(playerHand) - - if handValue == 21: - blackjackHand = True - else: - blackjackHand = False - - newHand = {"hand":playerHand, "bet":bet, "standing":False, - "busted":False, "blackjack":blackjackHand, "hit":True, - "doubled":False, "split":0, "other hand":{}, - "third hand":{}, "fourth hand":{}} - - function = {"$set":{f"user hands.{user}":newHand}} - collection.update_one({"_id":channel}, function) - - enterGameText = "entered the game with a bet of" - betText = f"{bet} GwendoBucks" - sendMessage = f"{userName} {enterGameText} {betText}" - logMessage = sendMessage - - self.bot.log(sendMessage) - await ctx.send(logMessage) - - # Starts a game of blackjack - async def start(self, ctx): - await self.bot.defer(ctx) - channel = str(ctx.channel_id) - blackjackMinCards = 50 - blackjackDecks = 4 - - await ctx.send("Starting a new game of blackjack") - cardsLeft = 0 - cards = self.bot.database["blackjack cards"].find_one({"_id":channel}) - if cards != None: - cardsLeft = len(cards["cards"]) - - # Shuffles if not enough cards - if cardsLeft < blackjackMinCards: - self.blackjackShuffle(blackjackDecks, channel) - self.bot.log("Shuffling the blackjack deck...", channel) - await ctx.channel.send("Shuffling the deck...") - - game = self.bot.database["blackjack games"].find_one({"_id":channel}) - - self.bot.log("Trying to start a blackjack game in "+channel) - gameStarted = False - - if game == None: - - dealerHand = [self.drawCard(channel),self.drawCard(channel)] - gameID = datetime.datetime.now().strftime('%Y%m%d%H%M%S') - - newGame = {"_id":channel,"dealer hand": dealerHand,"dealer busted":False,"dealer blackjack":False,"user hands": {},"all standing":False,"round":0,"gameID":gameID} - - if self.calcHandValue(dealerHand) == 21: - newGame["dealer blackjack"] = True - - self.bot.database["blackjack games"].insert_one(newGame) - - copyfile("resources/games/blackjackTable.png","resources/games/blackjackTables/blackjackTable"+channel+".png") - - gameStarted = True - - if gameStarted: - sendMessage = "Blackjack game started. Use \"/blackjack bet [amount]\" to enter the game within the next 30 seconds." - await ctx.channel.send(sendMessage) - filePath = f"resources/games/blackjackTables/blackjackTable{channel}.png" - - oldImage = await ctx.channel.send(file = discord.File(filePath)) - - with open("resources/games/oldImages/blackjack"+channel, "w") as f: - f.write(str(oldImage.id)) - - await asyncio.sleep(30) - - gamedone = False - - game = self.bot.database["blackjack games"].find_one({"_id":str(channel)}) - - if len(game["user hands"]) == 0: - gamedone = True - await ctx.channel.send("No one entered the game. Ending the game.") - gameID = game["gameID"] - - # Loop of game rounds - if gamedone == False: - self.bot.log("start() calling blackjackLoop()", channel) - await self.blackjackLoop(ctx.channel,1,gameID) - else: - new_message = self.blackjackFinish(channel) - await ctx.channel.send(new_message) - else: - await ctx.channel.send("There's already a blackjack game going on. Try again in a few minutes.") - self.bot.log("There was already a game going on") - - # Ends the game and calculates winnings - def blackjackFinish(self,channel): - finalWinnings = "*Final Winnings:*\n" - - game = self.bot.database["blackjack games"].find_one({"_id":channel}) - - dealerValue = self.calcHandValue(game["dealer hand"]) - dealerBlackjack = game["dealer blackjack"] - dealerBusted = game["dealer busted"] - - try: - for user in game["user hands"]: - - winnings, netWinnings, reason = self.calcWinnings(game["user hands"][user],dealerValue,True,dealerBlackjack,dealerBusted) - - if winnings < 0: - if winnings == -1: - finalWinnings += self.bot.databaseFuncs.getName(user)+" lost "+str(-1 * winnings)+" GwendoBuck "+reason+"\n" - else: - finalWinnings += self.bot.databaseFuncs.getName(user)+" lost "+str(-1 * winnings)+" GwendoBucks "+reason+"\n" - else: - if winnings == 1: - finalWinnings += self.bot.databaseFuncs.getName(user)+" won "+str(winnings)+" GwendoBuck "+reason+"\n" - else: - finalWinnings += self.bot.databaseFuncs.getName(user)+" won "+str(winnings)+" GwendoBucks "+reason+"\n" - - self.bot.money.addMoney(user,netWinnings) - - except: - self.bot.log("Error calculating winnings (error code 1311)") - - self.bot.database["blackjack games"].delete_one({"_id":channel}) - - return finalWinnings - - def calcWinnings(self,hand, dealerValue, topLevel, dealerBlackjack, dealerBusted): - self.bot.log("Calculating winnings") - reason = "" - bet = hand["bet"] - winnings = -1 * bet - netWinnings = 0 - handValue = self.calcHandValue(hand["hand"]) - - if hand["blackjack"] and dealerBlackjack == False: - reason += "(blackjack)" - winnings += math.floor(2.5 * bet) - netWinnings += math.floor(2.5 * bet) - elif dealerBlackjack: - reason += "(dealer blackjack)" - elif hand["busted"]: - reason += "(busted)" - else: - if dealerBusted: - reason = "(dealer busted)" - winnings += 2 * bet - netWinnings += 2 * bet - elif handValue > dealerValue: - winnings += 2 * bet - netWinnings += 2 * bet - reason = "(highest value)" - elif handValue == dealerValue: - reason = "(pushed)" - winnings += bet - netWinnings += bet - else: - reason = "(highest value)" - - if topLevel: - if hand["split"] >= 1: - winningsTemp, netWinningsTemp, reasonTemp = self.calcWinnings(hand["other hand"],dealerValue,False,dealerBlackjack,dealerBusted) - winnings += winningsTemp - netWinnings += netWinningsTemp - reason += reasonTemp - if hand["split"] >= 2: - winningsTemp, netWinningsTemp, reasonTemp = self.calcWinnings(hand["third hand"],dealerValue,False,dealerBlackjack,dealerBusted) - winnings += winningsTemp - netWinnings += netWinningsTemp - reason += reasonTemp - if hand["split"] >= 3: - winningsTemp, netWinningsTemp, reasonTemp = self.calcWinnings(hand["fourth hand"],dealerValue,False,dealerBlackjack,dealerBusted) - winnings += winningsTemp - netWinnings += netWinningsTemp - reason += reasonTemp - - return winnings, netWinnings, reason - - def getHandNumber(self, user,handNumber): - try: - hand = None - - if user["split"] == 0: - hand = user - handNumber = 0 - else: - if handNumber != 0: - if handNumber == 1: - hand = user - elif handNumber == 2: - hand = user["other hand"] - elif handNumber == 3: - hand = user["third hand"] - elif handNumber == 4: - hand = user["fourth hand"] - - return hand, handNumber - except: - self.bot.log("Problem with getHandNumber() (error code 1322)") - - def isRoundDone(self,game): - roundDone = True - - for person in game["user hands"].values(): - if person["hit"] == False and person["standing"] == False: - roundDone = False - - if person["split"] > 0: - if person["other hand"]["hit"] == False and person["other hand"]["standing"] == False: - roundDone = False - - if person["split"] > 1: - if person["third hand"]["hit"] == False and person["third hand"]["standing"] == False: - roundDone = False - - if person["split"] > 2: - if person["fourth hand"]["hit"] == False and person["fourth hand"]["standing"] == False: - roundDone = False - - return roundDone - - # Loop of blackjack game rounds - async def blackjackLoop(self,channel,gameRound,gameID): - self.bot.log("Loop "+str(gameRound),str(channel.id)) - - with open("resources/games/oldImages/blackjack"+str(channel.id), "r") as f: - oldImage = await channel.fetch_message(int(f.read())) - - new_message, allStanding, gamedone = self.blackjackContinue(str(channel.id)) - if new_message != "": - self.bot.log(new_message,str(channel.id)) - await channel.send(new_message) - if gamedone == False: - await oldImage.delete() - oldImage = await channel.send(file = discord.File("resources/games/blackjackTables/blackjackTable"+str(channel.id)+".png")) - with open("resources/games/oldImages/blackjack"+str(channel.id), "w") as f: - f.write(str(oldImage.id)) - - try: - if allStanding: - await asyncio.sleep(5) - else: - await asyncio.sleep(120) - except: - self.bot.log("Loop "+str(gameRound)+" interrupted (error code 1321)") - - game = self.bot.database["blackjack games"].find_one({"_id":str(channel.id)}) - - if game != None: - realRound = game["round"] - realGameID = game["gameID"] - - if gameRound == realRound and realGameID == gameID: - if gamedone == False: - self.bot.log("Loop "+str(gameRound)+" calling self.blackjackLoop()",str(channel.id)) - await self.blackjackLoop(channel,gameRound+1,gameID) - else: - try: - new_message = self.blackjackFinish(str(channel.id)) - except: - self.bot.log("Something fucked up (error code 1310)") - await channel.send(new_message) - else: - self.bot.log("Ending loop on round "+str(gameRound),str(channel.id)) - else: - self.bot.log("Ending loop on round "+str(gameRound),str(channel.id)) - - # Returning current hi-lo value - async def hilo(self, ctx): - channel = ctx.channel_id - data = self.bot.database["hilo"].find_one({"_id":str(channel)}) - if data != None: - hilo = str(data["hilo"]) - else: - hilo = "0" - await ctx.send(f"Hi-lo value: {hilo}", hidden=True) - - # Shuffles the blackjack deck - async def shuffle(self, ctx): - blackjackDecks = 4 - channel = ctx.channel_id - self.blackjackShuffle(blackjackDecks,str(channel)) - self.bot.log("Shuffling the blackjack deck...",str(channel)) - await ctx.send("Shuffling the deck...") - - - # Tells you the amount of cards left - async def cards(self, ctx): - channel = ctx.channel_id - cardsLeft = 0 - cards = self.bot.database["blackjack cards"].find_one({"_id":str(channel)}) - if cards != None: - cardsLeft = len(cards["cards"]) - - decksLeft = round(cardsLeft/52,1) - await ctx.send(f"Cards left:\n{cardsLeft} cards, {decksLeft} decks", hidden=True) - -class DrawBlackjack(): - def __init__(self,bot): - self.bot = bot - self.BORDER = 100 - self.PLACEMENT = [0,0] - self.ROTATION = 0 - - def drawImage(self,channel): - self.bot.log("Drawing blackjack table",channel) - game = self.bot.database["blackjack games"].find_one({"_id":channel}) - - fnt = ImageFont.truetype('resources/fonts/futura-bold.ttf', 50) - fntSmol = ImageFont.truetype('resources/fonts/futura-bold.ttf', 40) - self.BORDERSmol = int(self.BORDER/3.5) - - table = Image.open("resources/games/blackjackTable.png") - self.PLACEMENT = [2,1,3,0,4] - textImage = ImageDraw.Draw(table) - hands = game["user hands"] - - dealerBusted = game["dealer busted"] - dealerBlackjack = game["dealer blackjack"] - - try: - if game["all standing"] == False: - dealerHand = self.drawHand(game["dealer hand"],True,False,False) - else: - dealerHand = self.drawHand(game["dealer hand"],False,dealerBusted,dealerBlackjack) - except: - self.bot.log("Error drawing dealer hand (error code 1341a)") - - table.paste(dealerHand,(800-self.BORDERSmol,20-self.BORDERSmol),dealerHand) - - for x in range(len(hands)): - key, value = list(hands.items())[x] - key = self.bot.databaseFuncs.getName(key) - #self.bot.log("Drawing "+key+"'s hand") - userHand = self.drawHand(value["hand"],False,value["busted"],value["blackjack"]) - try: - if value["split"] == 3: - table.paste(userHand,(32-self.BORDERSmol+(384*self.PLACEMENT[x]),280-self.BORDERSmol),userHand) - userOtherHand = self.drawHand(value["other hand"]["hand"],False,value["other hand"]["busted"],value["other hand"]["blackjack"]) - table.paste(userOtherHand,(32-self.BORDERSmol+(384*self.PLACEMENT[x]),420-self.BORDERSmol),userOtherHand) - userThirdHand = self.drawHand(value["third hand"]["hand"],False,value["third hand"]["busted"],value["third hand"]["blackjack"]) - table.paste(userThirdHand,(32-self.BORDERSmol+(384*self.PLACEMENT[x]),560-self.BORDERSmol),userThirdHand) - userFourthHand = self.drawHand(value["fourth hand"]["hand"],False,value["fourth hand"]["busted"],value["fourth hand"]["blackjack"]) - table.paste(userFourthHand,(32-self.BORDERSmol+(384*self.PLACEMENT[x]),700-self.BORDERSmol),userFourthHand) - elif value["split"] == 2: - table.paste(userHand,(32-self.BORDERSmol+(384*self.PLACEMENT[x]),420-self.BORDERSmol),userHand) - userOtherHand = self.drawHand(value["other hand"]["hand"],False,value["other hand"]["busted"],value["other hand"]["blackjack"]) - table.paste(userOtherHand,(32-self.BORDERSmol+(384*self.PLACEMENT[x]),560-self.BORDERSmol),userOtherHand) - userThirdHand = self.drawHand(value["third hand"]["hand"],False,value["third hand"]["busted"],value["third hand"]["blackjack"]) - table.paste(userThirdHand,(32-self.BORDERSmol+(384*self.PLACEMENT[x]),700-self.BORDERSmol),userThirdHand) - elif value["split"] == 1: - table.paste(userHand,(32-self.BORDERSmol+(384*self.PLACEMENT[x]),560-self.BORDERSmol),userHand) - userOtherHand = self.drawHand(value["other hand"]["hand"],False,value["other hand"]["busted"],value["other hand"]["blackjack"]) - table.paste(userOtherHand,(32-self.BORDERSmol+(384*self.PLACEMENT[x]),700-self.BORDERSmol),userOtherHand) - else: - table.paste(userHand,(32-self.BORDERSmol+(384*self.PLACEMENT[x]),680-self.BORDERSmol),userHand) - except: - self.bot.log("Error drawing player hands (error code 1341b)") - - textWidth = fnt.getsize(key)[0] - if textWidth < 360: - textImage.text((32+(384*self.PLACEMENT[x])+117-int(textWidth/2)-3,1010-3),key,fill=(0,0,0), font=fnt) - textImage.text((32+(384*self.PLACEMENT[x])+117-int(textWidth/2)+3,1010-3),key,fill=(0,0,0), font=fnt) - textImage.text((32+(384*self.PLACEMENT[x])+117-int(textWidth/2)-3,1010+3),key,fill=(0,0,0), font=fnt) - textImage.text((32+(384*self.PLACEMENT[x])+117-int(textWidth/2)+3,1010+3),key,fill=(0,0,0), font=fnt) - textImage.text((32+(384*self.PLACEMENT[x])+117-int(textWidth/2),1005),key,fill=(255,255,255), font=fnt) - else: - textWidth = fntSmol.getsize(key)[0] - textImage.text((32+(384*self.PLACEMENT[x])+117-int(textWidth/2)-2,1020-2),key,fill=(0,0,0), font=fntSmol) - textImage.text((32+(384*self.PLACEMENT[x])+117-int(textWidth/2)+2,1020-2),key,fill=(0,0,0), font=fntSmol) - textImage.text((32+(384*self.PLACEMENT[x])+117-int(textWidth/2)-2,1020+2),key,fill=(0,0,0), font=fntSmol) - textImage.text((32+(384*self.PLACEMENT[x])+117-int(textWidth/2)+2,1020+2),key,fill=(0,0,0), font=fntSmol) - textImage.text((32+(384*self.PLACEMENT[x])+117-int(textWidth/2),1015),key,fill=(255,255,255), font=fntSmol) - - self.bot.log("Saving table image") - table.save("resources/games/blackjackTables/blackjackTable"+channel+".png") - - return - - def drawHand(self, hand, dealer, busted, blackjack): - self.bot.log("Drawing hand "+str(hand)+", "+str(busted)+", "+str(blackjack)) - fnt = ImageFont.truetype('resources/fonts/futura-bold.ttf', 200) - fnt2 = ImageFont.truetype('resources/fonts/futura-bold.ttf', 120) - length = len(hand) - background = Image.new("RGBA", ((self.BORDER*2)+691+(125*(length-1)),(self.BORDER*2)+1065),(0,0,0,0)) - textImage = ImageDraw.Draw(background) - - if dealer: - img = Image.open("resources/games/cards/"+hand[0].upper()+".png") - #self.ROTATION = (random.randint(-20,20)/10.0) - img = img.rotate(self.ROTATION,expand = 1) - #self.PLACEMENT = [random.randint(-20,20),random.randint(-20,20)] - background.paste(img,(self.BORDER+self.PLACEMENT[0],self.BORDER+self.PLACEMENT[1]),img) - img = Image.open("resources/games/cards/red_back.png") - #self.ROTATION = (random.randint(-20,20)/10.0) - img = img.rotate(self.ROTATION,expand = 1) - #self.PLACEMENT = [random.randint(-20,20),random.randint(-20,20)] - background.paste(img,(125+self.BORDER+self.PLACEMENT[0],self.BORDER+self.PLACEMENT[1]),img) - else: - for x in range(length): - img = Image.open("resources/games/cards/"+hand[x].upper()+".png") - #self.ROTATION = (random.randint(-20,20)/10.0) - img = img.rotate(self.ROTATION,expand = 1) - #self.PLACEMENT = [random.randint(-20,20),random.randint(-20,20)] - background.paste(img,(self.BORDER+(x*125)+self.PLACEMENT[0],self.BORDER+self.PLACEMENT[1]),img) - - w, h = background.size - textHeight = 290+self.BORDER - - #self.bot.log("Drawing busted/blackjack") - if busted: - textWidth = fnt.getsize("BUSTED")[0] - textImage.text((int(w/2)-int(textWidth/2)-10,textHeight+20-10),"BUSTED",fill=(0,0,0), font=fnt) - textImage.text((int(w/2)-int(textWidth/2)+10,textHeight+20-10),"BUSTED",fill=(0,0,0), font=fnt) - textImage.text((int(w/2)-int(textWidth/2)-10,textHeight+20+10),"BUSTED",fill=(0,0,0), font=fnt) - textImage.text((int(w/2)-int(textWidth/2)+10,textHeight+20+10),"BUSTED",fill=(0,0,0), font=fnt) - textImage.text((int(w/2)-int(textWidth/2)-5,textHeight-5),"BUSTED",fill=(255,255,255), font=fnt) - textImage.text((int(w/2)-int(textWidth/2)+5,textHeight-5),"BUSTED",fill=(255,255,255), font=fnt) - textImage.text((int(w/2)-int(textWidth/2)-5,textHeight+5),"BUSTED",fill=(255,255,225), font=fnt) - textImage.text((int(w/2)-int(textWidth/2)+5,textHeight+5),"BUSTED",fill=(255,255,255), font=fnt) - textImage.text((int(w/2)-int(textWidth/2),textHeight),"BUSTED",fill=(255,50,50), font=fnt) - elif blackjack: - textWidth = fnt2.getsize("BLACKJACK")[0] - textImage.text((int(w/2)-int(textWidth/2)-6,textHeight+20-6),"BLACKJACK",fill=(0,0,0), font=fnt2) - textImage.text((int(w/2)-int(textWidth/2)+6,textHeight+20-6),"BLACKJACK",fill=(0,0,0), font=fnt2) - textImage.text((int(w/2)-int(textWidth/2)-6,textHeight+20+6),"BLACKJACK",fill=(0,0,0), font=fnt2) - textImage.text((int(w/2)-int(textWidth/2)+6,textHeight+20+6),"BLACKJACK",fill=(0,0,0), font=fnt2) - textImage.text((int(w/2)-int(textWidth/2)-3,textHeight-3),"BLACKJACK",fill=(255,255,255), font=fnt2) - textImage.text((int(w/2)-int(textWidth/2)+3,textHeight-3),"BLACKJACK",fill=(255,255,255), font=fnt2) - textImage.text((int(w/2)-int(textWidth/2)-3,textHeight+3),"BLACKJACK",fill=(255,255,255), font=fnt2) - textImage.text((int(w/2)-int(textWidth/2)+3,textHeight+3),"BLACKJACK",fill=(255,255,255), font=fnt2) - textImage.text((int(w/2)-int(textWidth/2),textHeight),"BLACKJACK",fill=(155,123,0), font=fnt2) - - #self.bot.log("Returning resized image") - return background.resize((int(w/3.5),int(h/3.5)),resample=Image.BILINEAR) - diff --git a/funcs/games/connectFour.py b/funcs/games/connectFour.py deleted file mode 100644 index 88a8024..0000000 --- a/funcs/games/connectFour.py +++ /dev/null @@ -1,583 +0,0 @@ -import random -import copy -import math -import discord - -from PIL import Image, ImageDraw, ImageFont - -class ConnectFour(): - def __init__(self,bot): - self.bot = bot - self.draw = drawConnectFour(bot) - self.AISCORES = { - "middle": 3, - "two in a row": 10, - "three in a row": 50, - "enemy two in a row": -35, - "enemy three in a row": -200, - "enemy win": -10000, - "win": 10000, - "avoid losing": 100 - } - - self.ROWCOUNT = 6 - self.COLUMNCOUNT = 7 - - # Starts the game - async def start(self, ctx, opponent): - await self.bot.defer(ctx) - user = f"#{ctx.author.id}" - channel = str(ctx.channel_id) - game = self.bot.database["connect 4 games"].find_one({"_id":channel}) - - startedGame = False - canStart = True - - if game != None: - sendMessage = "There's already a connect 4 game going on in this channel" - logMessage = "There was already a game going on" - canStart = False - else: - if type(opponent) == int: - # Opponent is Gwendolyn - if opponent in range(1, 6): - difficulty = int(opponent) - diffText = f" with difficulty {difficulty}" - opponent = f"#{self.bot.user.id}" - else: - sendMessage = "Difficulty doesn't exist" - logMessage = "They tried to play against a difficulty that doesn't exist" - canStart = False - - elif type(opponent) == discord.member.Member: - if opponent.bot: - # User has challenged a bot - if opponent == self.bot.user: - # It was Gwendolyn - difficulty = 3 - diffText = f" with difficulty {difficulty}" - opponent = f"#{self.bot.user.id}" - else: - sendMessage = "You can't challenge a bot!" - logMessage = "They tried to challenge a bot" - canStart = False - else: - # Opponent is another player - if ctx.author != opponent: - opponent = f"#{opponent.id}" - difficulty = 5 - diffText = "" - else: - sendMessage = "You can't play against yourself" - logMessage = "They tried to play against themself" - canStart = False - - if canStart: - board = [[0 for _ in range(self.COLUMNCOUNT)] for _ in range(self.ROWCOUNT)] - players = [user, opponent] - random.shuffle(players) - - newGame = {"_id":channel, "board": board, "winner":0, - "win direction":"", "win coordinates":[0, 0], - "players":players, "turn":0, "difficulty":difficulty} - - self.bot.database["connect 4 games"].insert_one(newGame) - - self.draw.drawImage(channel) - - gwendoTurn = (players[0] == f"#{self.bot.user.id}") - startedGame = True - - opponentName = self.bot.databaseFuncs.getName(opponent) - turnName = self.bot.databaseFuncs.getName(players[0]) - - startedText = f"Started game against {opponentName}{diffText}." - turnText = f"It's {turnName}'s turn" - sendMessage = f"{startedText} {turnText}" - logMessage = "They started a game" - - self.bot.log(logMessage) - await ctx.send(sendMessage) - - # Sets the whole game in motion - if startedGame: - filePath = f"resources/games/connect4Boards/board{ctx.channel_id}.png" - oldImage = await ctx.channel.send(file = discord.File(filePath)) - - with open(f"resources/games/oldImages/connectFour{ctx.channel_id}", "w") as f: - f.write(str(oldImage.id)) - - if gwendoTurn: - await self.connectFourAI(ctx) - else: - reactions = ["1️⃣","2️⃣","3️⃣","4️⃣","5️⃣","6️⃣","7️⃣"] - for reaction in reactions: - await oldImage.add_reaction(reaction) - - # Places a piece at the lowest available point in a specific column - async def placePiece(self, ctx, user, column): - channel = str(ctx.channel.id) - game = self.bot.database["connect 4 games"].find_one({"_id":channel}) - playerNumber = game["players"].index(user)+1 - userName = self.bot.databaseFuncs.getName(user) - placedPiece = False - - if game is None: - sendMessage = "There's no game in this channel" - logMessage = "There was no game in the channel" - else: - board = game["board"] - board = self.placeOnBoard(board, playerNumber, column) - - if board is None: - sendMessage = "There isn't any room in that column" - logMessage = "There wasn't any room in the column" - else: - self.bot.database["connect 4 games"].update_one({"_id":channel},{"$set":{"board":board}}) - turn = (game["turn"]+1)%2 - self.bot.database["connect 4 games"].update_one({"_id":channel},{"$set":{"turn":turn}}) - - self.bot.log("Checking for win") - won, winDirection, winCoordinates = self.isWon(board) - - if won != 0: - gameWon = True - self.bot.database["connect 4 games"].update_one({"_id":channel},{"$set":{"winner":won}}) - self.bot.database["connect 4 games"].update_one({"_id":channel},{"$set":{"win direction":winDirection}}) - self.bot.database["connect 4 games"].update_one({"_id":channel}, - {"$set":{"win coordinates":winCoordinates}}) - - sendMessage = f"{userName} placed a piece in column {column+1} and won." - logMessage = f"{userName} won" - winAmount = int(game["difficulty"])**2+5 - if game["players"][won-1] != f"#{self.bot.user.id}": - sendMessage += " Adding "+str(winAmount)+" GwendoBucks to their account" - elif 0 not in board[0]: - gameWon = True - sendMessage = "It's a draw!" - logMessage = "The game ended in a draw" - else: - gameWon = False - otherUserName = self.bot.databaseFuncs.getName(game["players"][turn]) - sendMessage = f"{userName} placed a piece in column {column+1}. It's now {otherUserName}'s turn" - logMessage = "They placed the piece" - - gwendoTurn = (game["players"][turn] == f"#{self.bot.user.id}") - - placedPiece = True - - await ctx.channel.send(sendMessage) - self.bot.log(logMessage) - - if placedPiece: - self.draw.drawImage(channel) - - with open(f"resources/games/oldImages/connectFour{channel}", "r") as f: - oldImage = await ctx.channel.fetch_message(int(f.read())) - - if oldImage is not None: - await oldImage.delete() - else: - self.bot.log("The old image was already deleted") - - filePath = f"resources/games/connect4Boards/board{channel}.png" - oldImage = await ctx.channel.send(file = discord.File(filePath)) - - if gameWon: - self.endGame(channel) - else: - with open(f"resources/games/oldImages/connectFour{channel}", "w") as f: - f.write(str(oldImage.id)) - if gwendoTurn: - await self.connectFourAI(ctx) - else: - reactions = ["1️⃣","2️⃣","3️⃣","4️⃣","5️⃣","6️⃣","7️⃣"] - for reaction in reactions: - await oldImage.add_reaction(reaction) - - # Returns a board where a piece has been placed in the column - def placeOnBoard(self, board, player, column): - placementX, placementY = -1, column - - for x, line in enumerate(board): - if line[column] == 0: - placementX = x - - board[placementX][placementY] = player - - if placementX == -1: - return None - else: - return board - - def endGame(self, channel): - game = self.bot.database["connect 4 games"].find_one({"_id":channel}) - - winner = game["winner"] - if winner != 0: - if game["players"][winner-1] != f"#{self.bot.user.id}": - difficulty = int(game["difficulty"]) - reward = difficulty**2 + 5 - self.bot.money.addMoney(game["players"][winner-1], reward) - - self.bot.databaseFuncs.deleteGame("connect 4 games", channel) - - # Parses command - async def surrender(self, ctx): - 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"]: - loserIndex = game["players"].index(f"#{ctx.author.id}") - winnerIndex = (loserIndex+1)%2 - winnerID = game["players"][winnerIndex] - winnerName = self.bot.databaseFuncs.getName(winnerID) - - sendMessage = f"{ctx.author.display_name} surrenders." - sendMessage += f" This means {winnerName} is the winner." - if winnerID != f"#{self.bot.user.id}": - difficulty = int(game["difficulty"]) - reward = difficulty**2 + 5 - sendMessage += f" Adding {reward} to their account" - - await ctx.send(sendMessage) - with open(f"resources/games/oldImages/connectFour{channel}", "r") as f: - oldImage = await ctx.channel.fetch_message(int(f.read())) - - if oldImage is not None: - await oldImage.delete() - else: - self.bot.log("The old image was already deleted") - - self.endGame(channel) - else: - await ctx.send("You can't surrender when you're not a player") - - # Checks if someone has won the game and returns the winner - def isWon(self, board): - won = 0 - winDirection = "" - winCoordinates = [0,0] - - for row in range(self.ROWCOUNT): - for place in range(self.COLUMNCOUNT): - if won == 0: - piecePlayer = board[row][place] - if piecePlayer != 0: - # Checks horizontal - if place <= self.COLUMNCOUNT-4: - pieces = [board[row][place+1],board[row][place+2],board[row][place+3]] - else: - pieces = [0] - - if all(x == piecePlayer for x in pieces): - won = piecePlayer - winDirection = "h" - winCoordinates = [row,place] - - # Checks vertical - if row <= self.ROWCOUNT-4: - pieces = [board[row+1][place],board[row+2][place],board[row+3][place]] - else: - pieces = [0] - - if all(x == piecePlayer for x in pieces): - won = piecePlayer - winDirection = "v" - winCoordinates = [row,place] - - # Checks right diagonal - if row <= self.ROWCOUNT-4 and place <= self.COLUMNCOUNT-4: - pieces = [board[row+1][place+1],board[row+2][place+2],board[row+3][place+3]] - else: - pieces = [0] - - if all(x == piecePlayer for x in pieces): - won = piecePlayer - winDirection = "r" - winCoordinates = [row,place] - - # Checks left diagonal - if row <= self.ROWCOUNT-4 and place >= 3: - pieces = [board[row+1][place-1],board[row+2][place-2],board[row+3][place-3]] - else: - pieces = [0] - - if all(x == piecePlayer for x in pieces): - won = piecePlayer - winDirection = "l" - winCoordinates = [row,place] - - - return won, winDirection, winCoordinates - - # Plays as the AI - async def connectFourAI(self, ctx): - 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"] - - scores = [-math.inf for _ in range(self.COLUMNCOUNT)] - for column in range(self.COLUMNCOUNT): - testBoard = copy.deepcopy(board) - testBoard = self.placeOnBoard(testBoard,player,column) - if testBoard != None: - scores[column] = await self.minimax(testBoard,difficulty,player%2+1,player,-math.inf,math.inf,False) - self.bot.log(f"Best score for column {column} is {scores[column]}") - - possibleScores = scores.copy() - - while (min(possibleScores) <= (max(possibleScores) - max(possibleScores)/10)) and len(possibleScores) != 1: - possibleScores.remove(min(possibleScores)) - - highest_score = random.choice(possibleScores) - - bestColumns = [i for i, x in enumerate(scores) if x == highest_score] - placement = random.choice(bestColumns) - - await self.placePiece(ctx, f"#{self.bot.user.id}", placement) - - # Calculates points for a board - def AICalcPoints(self,board,player): - score = 0 - otherPlayer = player%2+1 - - # Adds points for middle placement - # Checks horizontal - for row in range(self.ROWCOUNT): - if board[row][3] == player: - score += self.AISCORES["middle"] - rowArray = [int(i) for i in list(board[row])] - for place in range(self.COLUMNCOUNT-3): - window = rowArray[place:place+4] - score += self.evaluateWindow(window,player,otherPlayer) - - # Checks Vertical - for column in range(self.COLUMNCOUNT): - columnArray = [int(i[column]) for i in list(board)] - for place in range(self.ROWCOUNT-3): - window = columnArray[place:place+4] - score += self.evaluateWindow(window,player,otherPlayer) - - # Checks right diagonal - for row in range(self.ROWCOUNT-3): - for place in range(self.COLUMNCOUNT-3): - window = [board[row][place],board[row+1][place+1],board[row+2][place+2],board[row+3][place+3]] - score += self.evaluateWindow(window,player,otherPlayer) - - for place in range(3,self.COLUMNCOUNT): - window = [board[row][place],board[row+1][place-1],board[row+2][place-2],board[row+3][place-3]] - score += self.evaluateWindow(window,player,otherPlayer) - - - ## Checks if anyone has won - #won = isWon(board)[0] - - ## Add points if AI wins - #if won == player: - # score += self.AISCORES["win"] - - return score - - def evaluateWindow(self, window,player,otherPlayer): - if window.count(player) == 4: - return self.AISCORES["win"] - elif window.count(player) == 3 and window.count(0) == 1: - return self.AISCORES["three in a row"] - elif window.count(player) == 2 and window.count(0) == 2: - return self.AISCORES["two in a row"] - elif window.count(otherPlayer) == 4: - return self.AISCORES["enemy win"] - else: - return 0 - - async def minimax(self, board, depth, player , originalPlayer, alpha, beta, maximizingPlayer): - #terminal = ((0 not in board[0]) or (isWon(board)[0] != 0)) - terminal = 0 not in board[0] - points = self.AICalcPoints(board,originalPlayer) - # The depth is how many moves ahead the computer checks. This value is the difficulty. - if depth == 0 or terminal or (points > 5000 or points < -6000): - return points - if maximizingPlayer: - value = -math.inf - for column in range(0,self.COLUMNCOUNT): - testBoard = copy.deepcopy(board) - testBoard = self.placeOnBoard(testBoard,player,column) - if testBoard != None: - evaluation = await self.minimax(testBoard,depth-1,player%2+1,originalPlayer,alpha,beta,False) - if evaluation < -9000: evaluation += self.AISCORES["avoid losing"] - value = max(value,evaluation) - alpha = max(alpha,evaluation) - if beta <= alpha: break - return value - else: - value = math.inf - for column in range(0,self.COLUMNCOUNT): - testBoard = copy.deepcopy(board) - testBoard = self.placeOnBoard(testBoard,player,column) - if testBoard != None: - evaluation = await self.minimax(testBoard,depth-1,player%2+1,originalPlayer,alpha,beta,True) - if evaluation < -9000: evaluation += self.AISCORES["avoid losing"] - value = min(value,evaluation) - beta = min(beta,evaluation) - if beta <= alpha: break - return value - -class drawConnectFour(): - def __init__(self, bot): - self.bot = bot - - # Draws the whole thing - def drawImage(self, channel): - self.bot.log("Drawing connect four board") - game = self.bot.database["connect 4 games"].find_one({"_id":channel}) - - board = game["board"] - - border = 40 - gridBorder = 40 - cornerSize = 300 - boardOutlineSize = 10 - pieceOutlineSize = 10 - emptyOutlineSize = 0 - bottomBorder = 110 - exampleCircles = 100 - w, h = 2800,2400 - backgroundColor = (230,230,234,255) - boardOutlineColor = (0,0,0) - pieceOutlineColor = (244,244,248) - emptyOutlineColor = (0,0,0) - player1Color = (254,74,73) - player2Color = (254,215,102) - boardColor = (42,183,202) - placeSize = 300 - white = (255,255,255,160) - winBarColor = (250,250,250,255) - - fnt = ImageFont.truetype('resources/fonts/futura-bold.ttf', exampleCircles) - - boardSize = [w-(2*(border+gridBorder)),h-(2*(border+gridBorder))] - placeGridSize = [math.floor(boardSize[0]/7),math.floor(boardSize[1]/6)] - pieceStartx = (border+gridBorder)+math.floor(placeGridSize[0]/2)-math.floor(placeSize/2) - pieceStarty = (border+gridBorder)+math.floor(placeGridSize[1]/2)-math.floor(placeSize/2) - - if game["players"][0] == "Gwendolyn": - player1 = "Gwendolyn" - else: - player1 = self.bot.databaseFuncs.getName(game["players"][0]) - - if game["players"][1] == "Gwendolyn": - player2 = "Gwendolyn" - else: - player2 = self.bot.databaseFuncs.getName(game["players"][1]) - - - background = Image.new("RGB", (w,h+bottomBorder),backgroundColor) - d = ImageDraw.Draw(background,"RGBA") - - # This whole part was the easiest way to make a rectangle with rounded corners and an outline - # - Corners: - d.ellipse([(border,border),(border+cornerSize,border+cornerSize)],fill=boardColor,outline=boardOutlineColor,width=boardOutlineSize) - d.ellipse([(w-(border+cornerSize),h-(border+cornerSize)),(w-border,h-border)],fill=boardColor,outline=boardOutlineColor,width=boardOutlineSize) - d.ellipse([(border,h-(border+cornerSize)),(border+cornerSize,h-border)],fill=boardColor,outline=boardOutlineColor,width=boardOutlineSize) - d.ellipse([(w-(border+cornerSize),border),(w-border,border+cornerSize)],fill=boardColor,outline=boardOutlineColor,width=boardOutlineSize) - # - Rectangle: - d.rectangle([(border+math.floor(cornerSize/2),border),(w-(border+math.floor(cornerSize/2)),h-border)],fill=boardColor,outline=boardOutlineColor,width=boardOutlineSize) - d.rectangle([(border,border+math.floor(cornerSize/2)),(w-border,h-(border+math.floor(cornerSize/2)))],fill=boardColor,outline=boardOutlineColor,width=boardOutlineSize) - # - Removing outline on the inside: - d.rectangle([(border+math.floor(cornerSize/2),border+math.floor(cornerSize/2)),(w-(border+math.floor(cornerSize/2)),h-(border+math.floor(cornerSize/2)))],fill=boardColor) - d.rectangle([(border+math.floor(cornerSize/2),border+boardOutlineSize),(w-(border+math.floor(cornerSize/2)),h-(border+boardOutlineSize))],fill=boardColor) - d.rectangle([(border+boardOutlineSize,border+math.floor(cornerSize/2)),(w-(border+boardOutlineSize),h-(border+math.floor(cornerSize/2)))],fill=boardColor) - - for x, line in enumerate(board): - for y, piece in enumerate(line): - - if piece == 1: - pieceColor = player1Color - outlineWidth = pieceOutlineSize - outlineColor = pieceOutlineColor - elif piece == 2: - pieceColor = player2Color - outlineWidth = pieceOutlineSize - outlineColor = pieceOutlineColor - else: - pieceColor = backgroundColor - outlineWidth = emptyOutlineSize - outlineColor = emptyOutlineColor - - startx = pieceStartx + placeGridSize[0]*y - starty = pieceStarty + placeGridSize[1]*x - - d.ellipse([(startx,starty),(startx+placeSize,starty+placeSize)],fill=pieceColor,outline=outlineColor,width=outlineWidth) - - if game["winner"] != 0: - coordinates = game["win coordinates"] - startx = border + placeGridSize[0]*coordinates[1] + gridBorder - starty = border + placeGridSize[1]*coordinates[0] + gridBorder - a = (placeGridSize[0]*4-gridBorder-border)**2 - b = (placeGridSize[1]*4-gridBorder-border)**2 - diagonalLength = (math.sqrt(a+b))/placeGridSize[0] - diagonalAngle = math.degrees(math.atan(placeGridSize[1]/placeGridSize[0])) - - if game["win direction"] == "h": - winBar = Image.new("RGBA",(placeGridSize[0]*4,placeGridSize[1]),(0,0,0,0)) - winD = ImageDraw.Draw(winBar) - winD.ellipse([(0,0),(placeGridSize[0],placeGridSize[1])],fill=white) - winD.ellipse([((placeGridSize[0]*3),0),(placeGridSize[0]*4,placeGridSize[1])],fill=white) - winD.rectangle([(int(placeGridSize[0]*0.5),0),(int(placeGridSize[0]*3.5),placeGridSize[1])],fill=white) - - elif game["win direction"] == "v": - winBar = Image.new("RGBA",(placeGridSize[0],placeGridSize[1]*4),(0,0,0,0)) - winD = ImageDraw.Draw(winBar) - winD.ellipse([(0,0),(placeGridSize[0],placeGridSize[1])],fill=white) - winD.ellipse([(0,(placeGridSize[1]*3)),(placeGridSize[0],placeGridSize[1]*4)],fill=white) - winD.rectangle([0,(int(placeGridSize[1]*0.5)),(placeGridSize[0],int(placeGridSize[1]*3.5))],fill=white) - - elif game["win direction"] == "r": - winBar = Image.new("RGBA",(int(placeGridSize[0]*diagonalLength),placeGridSize[1]),(0,0,0,0)) - winD = ImageDraw.Draw(winBar) - winD.ellipse([(0,0),(placeGridSize[0],placeGridSize[1])],fill=white) - winD.ellipse([((placeGridSize[0]*(diagonalLength-1)),0),(placeGridSize[0]*diagonalLength,placeGridSize[1])],fill=white) - winD.rectangle([(int(placeGridSize[0]*0.5),0),(int(placeGridSize[0]*(diagonalLength-0.5)),placeGridSize[1])],fill=white) - winBar = winBar.rotate(-diagonalAngle,expand=1) - startx -= 90 - starty -= 100 - - elif game["win direction"] == "l": - winBar = Image.new("RGBA",(int(placeGridSize[0]*diagonalLength),placeGridSize[1]),(0,0,0,0)) - winD = ImageDraw.Draw(winBar) - winD.ellipse([(0,0),(placeGridSize[0],placeGridSize[1])],fill=white) - winD.ellipse([((placeGridSize[0]*(diagonalLength-1)),0),(placeGridSize[0]*diagonalLength,placeGridSize[1])],fill=white) - winD.rectangle([(int(placeGridSize[0]*0.5),0),(int(placeGridSize[0]*(diagonalLength-0.5)),placeGridSize[1])],fill=white) - winBar = winBar.rotate(diagonalAngle,expand=1) - startx -= placeGridSize[0]*3 + 90 - starty -= gridBorder + 60 - - - mask = winBar.copy()#.convert("L") - #mask.putalpha(128) - #mask.save("test.png") - - winBarImage = Image.new("RGBA",mask.size,color=winBarColor) - background.paste(winBarImage,(startx,starty),mask) - - # Bottom - textPadding = 20 - - exampleHeight = h - border + int((bottomBorder+border)/2) - int(exampleCircles/2) - d.ellipse([(border,exampleHeight),(border+exampleCircles),(exampleHeight+exampleCircles)],fill=player1Color,outline=boardOutlineColor,width=3) - d.text((border+exampleCircles+textPadding,exampleHeight),player1,font=fnt,fill=(0,0,0)) - - textWidth = fnt.getsize(player2)[0] - d.ellipse([(w-border-exampleCircles-textWidth-textPadding,exampleHeight),(w-border-textWidth-textPadding),(exampleHeight+exampleCircles)],fill=player2Color,outline=boardOutlineColor,width=3) - d.text((w-border-textWidth,exampleHeight),player2,font=fnt,fill=(0,0,0)) - - - background.save("resources/games/connect4Boards/board"+channel+".png") - diff --git a/funcs/games/gamesContainer.py b/funcs/games/gamesContainer.py deleted file mode 100644 index 203209a..0000000 --- a/funcs/games/gamesContainer.py +++ /dev/null @@ -1,17 +0,0 @@ -from .invest import Invest -from .trivia import Trivia -from .blackjack import Blackjack -from .connectFour import ConnectFour -from .hangman import Hangman -from .hex import HexGame - -class Games(): - def __init__(self, bot): - self.bot = bot - - self.invest = Invest(bot) - self.trivia = Trivia(bot) - self.blackjack = Blackjack(bot) - self.connectFour = ConnectFour(bot) - self.hangman = Hangman(bot) - self.hex = HexGame(bot) diff --git a/funcs/games/hangman.py b/funcs/games/hangman.py deleted file mode 100644 index f330000..0000000 --- a/funcs/games/hangman.py +++ /dev/null @@ -1,407 +0,0 @@ -import json, urllib, datetime, string, discord -import math, random - -from PIL import ImageDraw, Image, ImageFont - -class Hangman(): - def __init__(self,bot): - self.bot = bot - self.draw = DrawHangman(bot) - self.APIURL = "https://api.wordnik.com/v4/words.json/randomWords?hasDictionaryDef=true&minCorpusCount=5000&maxCorpusCount=-1&minDictionaryCount=1&maxDictionaryCount=-1&minLength=3&maxLength=11&limit=1&api_key=" - - async def start(self, ctx): - 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}) - userName = self.bot.databaseFuncs.getName(user) - startedGame = False - - if game == None: - apiKey = self.bot.credentials.wordnikKey - word = "-" - while "-" in word or "." in word: - with urllib.request.urlopen(self.APIURL+apiKey) as p: - word = list(json.load(p)[0]["word"].upper()) - self.bot.log("Found the word \""+"".join(word)+"\"") - guessed = [False] * len(word) - gameID = datetime.datetime.now().strftime('%Y%m%d%H%M%S') - newGame = {"_id":channel,"player" : user,"guessed letters" : [],"word" : word,"game ID" : gameID,"misses" : 0,"guessed" : guessed} - self.bot.database["hangman games"].insert_one(newGame) - - remainingLetters = list(string.ascii_uppercase) - - self.draw.drawImage(channel) - - logMessage = "Game started" - sendMessage = f"{userName} started game of hangman." - startedGame = True - else: - logMessage = "There was already a game going on" - sendMessage = "There's already a Hangman game going on in the channel" - - self.bot.log(logMessage) - await ctx.send(sendMessage) - - if startedGame: - filePath = f"resources/games/hangmanBoards/hangmanBoard{channel}.png" - newImage = await ctx.channel.send(file = discord.File(filePath)) - - blankMessage = await ctx.channel.send("_ _") - reactionMessages = {newImage : remainingLetters[:15], blankMessage : remainingLetters[15:]} - - oldMessages = f"{newImage.id}\n{blankMessage.id}" - - with open(f"resources/games/oldImages/hangman{channel}", "w") as f: - f.write(oldMessages) - - for message, letters in reactionMessages.items(): - for letter in letters: - emoji = chr(ord(letter)+127397) - await message.add_reaction(emoji) - - async def stop(self, ctx): - 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}) - - with open(f"resources/games/oldImages/hangman{channel}", "r") as f: - messages = f.read().splitlines() - - for message in messages: - oldMessage = await ctx.channel.fetch_message(int(message)) - self.bot.log("Deleting old message") - await oldMessage.delete() - - await ctx.send("Game stopped") - - async def guess(self, message, user, guess): - channel = str(message.channel.id) - game = self.bot.database["hangman games"].find_one({"_id":channel}) - - gameExists = (game != None) - singleLetter = (len(guess) == 1 and guess.isalpha()) - newGuess = (guess not in game["guessed letters"]) - validGuess = (gameExists and singleLetter and newGuess) - - if validGuess: - self.bot.log("Guessed the letter") - correctGuess = 0 - - for x, letter in enumerate(game["word"]): - if guess == letter: - correctGuess += 1 - self.bot.database["hangman games"].update_one({"_id":channel},{"$set":{"guessed."+str(x):True}}) - - if correctGuess == 0: - self.bot.database["hangman games"].update_one({"_id":channel},{"$inc":{"misses":1}}) - - self.bot.database["hangman games"].update_one({"_id":channel},{"$push":{"guessed letters":guess}}) - - remainingLetters = list(string.ascii_uppercase) - - game = self.bot.database["hangman games"].find_one({"_id":channel}) - - for letter in game["guessed letters"]: - remainingLetters.remove(letter) - - if correctGuess == 1: - sendMessage = f"Guessed {guess}. There was 1 {guess} in the word." - else: - sendMessage = f"Guessed {guess}. There were {correctGuess} {guess}s in the word." - - self.draw.drawImage(channel) - - if game["misses"] == 6: - self.bot.database["hangman games"].delete_one({"_id":channel}) - sendMessage += " You've guessed wrong six times and have lost the game." - remainingLetters = [] - elif all(i == True for i in game["guessed"]): - self.bot.database["hangman games"].delete_one({"_id":channel}) - self.bot.money.addMoney(user,15) - sendMessage += " You've guessed the word! Congratulations! Adding 15 GwendoBucks to your account" - remainingLetters = [] - - await message.channel.send(sendMessage) - - with open(f"resources/games/oldImages/hangman{channel}", "r") as f: - oldMessageIDs = f.read().splitlines() - - for oldID in oldMessageIDs: - oldMessage = await message.channel.fetch_message(int(oldID)) - self.bot.log("Deleting old message") - await oldMessage.delete() - - filePath = f"resources/games/hangmanBoards/hangmanBoard{channel}.png" - newImage = await message.channel.send(file = discord.File(filePath)) - - if len(remainingLetters) > 0: - if len(remainingLetters) > 15: - blankMessage = await message.channel.send("_ _") - reactionMessages = {newImage : remainingLetters[:15], blankMessage : remainingLetters[15:]} - else: - blankMessage = "" - reactionMessages = {newImage : remainingLetters} - - if blankMessage != "": - oldMessages = f"{newImage.id}\n{blankMessage.id}" - else: - oldMessages = str(newImage.id) - - with open(f"resources/games/oldImages/hangman{channel}", "w") as f: - f.write(oldMessages) - - for message, letters in reactionMessages.items(): - for letter in letters: - emoji = chr(ord(letter)+127397) - await message.add_reaction(emoji) - -class DrawHangman(): - def __init__(self,bot): - self.bot = bot - self.CIRCLESIZE = 120 - self.LINEWIDTH = 12 - - self.BODYSIZE =210 - self.LIMBSIZE = 60 - self.ARMPOSITION = 60 - - self.MANX = (self.LIMBSIZE*2)+self.LINEWIDTH*4 - self.MANY = (self.CIRCLESIZE+self.BODYSIZE+self.LIMBSIZE)+self.LINEWIDTH*4 - - self.LETTERLINELENGTH = 90 - self.LETTERLINEDISTANCE = 30 - - self.GALLOWX, self.GALLOWY = 360,600 - self.GOLDENRATIO = 1-(1 / ((1 + 5 ** 0.5) / 2)) - - LETTERSIZE = 75 - TEXTSIZE = 70 - self.FONT = ImageFont.truetype('resources/fonts/comic-sans-bold.ttf', LETTERSIZE) - self.SMALLFONT = ImageFont.truetype('resources/fonts/comic-sans-bold.ttf', TEXTSIZE) - - def calcDeviance(self,preDev,preDevAcc,posChange,maxmin,maxAcceleration): - devAcc = preDevAcc + random.uniform(-posChange,posChange) - if devAcc > maxmin * maxAcceleration: devAcc = maxmin * maxAcceleration - elif devAcc < -maxmin * maxAcceleration: devAcc = -maxmin * maxAcceleration - - dev = preDev + devAcc - if dev > maxmin: dev = maxmin - elif dev < -maxmin: dev = -maxmin - return dev, devAcc - - def badCircle(self): - background = Image.new("RGBA",(self.CIRCLESIZE+(self.LINEWIDTH*3),self.CIRCLESIZE+(self.LINEWIDTH*3)),color=(0,0,0,0)) - - d = ImageDraw.Draw(background,"RGBA") - middle = (self.CIRCLESIZE+(self.LINEWIDTH*3))/2 - devx = 0 - devy = 0 - devAccx = 0 - devAccy = 0 - start = random.randint(-100,-80) - degreesAmount = 360 + random.randint(-10,30) - - for degree in range(degreesAmount): - devx, devAccx = self.calcDeviance(devx,devAccx,self.LINEWIDTH/100,self.LINEWIDTH,0.03) - devy, devAccy = self.calcDeviance(devy,devAccy,self.LINEWIDTH/100,self.LINEWIDTH,0.03) - - x = middle + (math.cos(math.radians(degree+start)) * (self.CIRCLESIZE/2)) - (self.LINEWIDTH/2) + devx - y = middle + (math.sin(math.radians(degree+start)) * (self.CIRCLESIZE/2)) - (self.LINEWIDTH/2) + devy - - d.ellipse([(x,y),(x+self.LINEWIDTH,y+self.LINEWIDTH)],fill=(0,0,0,255)) - - return background - - def badLine(self, length, rotated = False): - if rotated: - w, h = length+self.LINEWIDTH*3, self.LINEWIDTH*3 - else: - w, h = self.LINEWIDTH*3,length+self.LINEWIDTH*3 - background = Image.new("RGBA",(w,h),color=(0,0,0,0)) - - d = ImageDraw.Draw(background,"RGBA") - devx = random.randint(-int(self.LINEWIDTH/3),int(self.LINEWIDTH/3)) - devy = 0 - devAccx = 0 - devAccy = 0 - - for pixel in range(length): - devx, devAccx = self.calcDeviance(devx,devAccx,self.LINEWIDTH/1000,self.LINEWIDTH,0.004) - devy, devAccy = self.calcDeviance(devy,devAccy,self.LINEWIDTH/1000,self.LINEWIDTH,0.004) - - if rotated: - x = self.LINEWIDTH + pixel + devx - y = self.LINEWIDTH + devy - else: - x = self.LINEWIDTH + devx - y = self.LINEWIDTH + pixel + devy - - d.ellipse([(x,y),(x+self.LINEWIDTH,y+self.LINEWIDTH)],fill=(0,0,0,255)) - - return background - - def drawMan(self, misses): - background = Image.new("RGBA",(self.MANX,self.MANY),color=(0,0,0,0)) - - if misses >= 1: - head = self.badCircle() - background.paste(head,(int((self.MANX-(self.CIRCLESIZE+(self.LINEWIDTH*3)))/2),0),head) - if misses >= 2: - body = self.badLine(self.BODYSIZE) - background.paste(body,(int((self.MANX-(self.LINEWIDTH*3))/2),self.CIRCLESIZE),body) - - if misses >= 3: - limbs = random.sample(["rl","ll","ra","la"],min(misses-2,4)) - else: limbs = [] - - for limb in limbs: - if limb == "ra": - limbDrawing = self.badLine(self.LIMBSIZE,True) - rotation = random.randint(-45,45) - xpos = int((self.MANX-(self.LINEWIDTH*3))/2) - rotationCompensation = min(-int(math.sin(math.radians(rotation))*(self.LIMBSIZE+(self.LINEWIDTH*3))),0) - ypos = self.CIRCLESIZE+self.ARMPOSITION + rotationCompensation - limbDrawing = limbDrawing.rotate(rotation,expand=1) - background.paste(limbDrawing,(xpos,ypos),limbDrawing) - elif limb == "la": - limbDrawing = self.badLine(self.LIMBSIZE,True) - rotation = random.randint(-45,45) - xpos = int((self.MANX-(self.LINEWIDTH*3))/2)-self.LIMBSIZE - rotationCompensation = min(int(math.sin(math.radians(rotation))*(self.LIMBSIZE+(self.LINEWIDTH*3))),0) - ypos = self.CIRCLESIZE+self.ARMPOSITION + rotationCompensation - limbDrawing = limbDrawing.rotate(rotation,expand=1) - background.paste(limbDrawing,(xpos,ypos),limbDrawing) - elif limb == "rl": - limbDrawing = self.badLine(self.LIMBSIZE,True) - rotation = random.randint(-15,15) - xpos = int((self.MANX-(self.LINEWIDTH*3))/2)-self.LINEWIDTH - ypos = self.CIRCLESIZE+self.BODYSIZE-self.LINEWIDTH - limbDrawing = limbDrawing.rotate(rotation-45,expand=1) - background.paste(limbDrawing,(xpos,ypos),limbDrawing) - elif limb == "ll": - limbDrawing = self.badLine(self.LIMBSIZE,True) - rotation = random.randint(-15,15) - limbDrawing = limbDrawing.rotate(rotation+45,expand=1) - xpos = int((self.MANX-(self.LINEWIDTH*3))/2)-limbDrawing.size[0]+self.LINEWIDTH*3 - ypos = self.CIRCLESIZE+self.BODYSIZE - background.paste(limbDrawing,(xpos,ypos),limbDrawing) - - return background - - def badText(self, text, big, color=(0,0,0,255)): - if big: font = self.FONT - else: font = self.SMALLFONT - w, h = font.getsize(text) - img = Image.new("RGBA",(w,h),color=(0,0,0,0)) - d = ImageDraw.Draw(img,"RGBA") - - d.text((0,0),text,font=font,fill=color) - return img - - def drawGallows(self): - background = Image.new("RGBA",(self.GALLOWX,self.GALLOWY),color=(0,0,0,0)) - - bottomLine = self.badLine(int(self.GALLOWX*0.75),True) - background.paste(bottomLine,(int(self.GALLOWX*0.125),self.GALLOWY-(self.LINEWIDTH*4)),bottomLine) - - lineTwo = self.badLine(self.GALLOWY-self.LINEWIDTH*6) - background.paste(lineTwo,(int(self.GALLOWX*(0.75*self.GOLDENRATIO)),self.LINEWIDTH*2),lineTwo) - - topLine = self.badLine(int(self.GALLOWY*0.30),True) - background.paste(topLine,(int(self.GALLOWX*(0.75*self.GOLDENRATIO))-self.LINEWIDTH,self.LINEWIDTH*3),topLine) - - lastLine = self.badLine(int(self.GALLOWY*0.125)) - background.paste(lastLine,((int(self.GALLOWX*(0.75*self.GOLDENRATIO))+int(self.GALLOWY*0.30)-self.LINEWIDTH),self.LINEWIDTH*3),lastLine) - return background - - def drawLetterLines(self, word,guessed,misses): - letterLines = Image.new("RGBA",((self.LETTERLINELENGTH+self.LETTERLINEDISTANCE)*len(word),self.LETTERLINELENGTH+self.LINEWIDTH*3),color=(0,0,0,0)) - for x, letter in enumerate(word): - line = self.badLine(self.LETTERLINELENGTH,True) - letterLines.paste(line,(x*(self.LETTERLINELENGTH+self.LETTERLINEDISTANCE),self.LETTERLINELENGTH),line) - if guessed[x]: - letterDrawing = self.badText(letter,True) - letterWidth = self.FONT.getsize(letter)[0] - letterx = int(x*(self.LETTERLINELENGTH+self.LETTERLINEDISTANCE)-(letterWidth/2)+(self.LETTERLINELENGTH*0.5)+(self.LINEWIDTH*2)) - letterLines.paste(letterDrawing,(letterx,0),letterDrawing) - elif misses == 6: - letterDrawing = self.badText(letter,True,(242,66,54)) - letterWidth = self.FONT.getsize(letter)[0] - letterx = int(x*(self.LETTERLINELENGTH+self.LETTERLINEDISTANCE)-(letterWidth/2)+(self.LETTERLINELENGTH*0.5)+(self.LINEWIDTH*2)) - letterLines.paste(letterDrawing,(letterx,0),letterDrawing) - - return letterLines - - def shortestDist(self,positions,newPosition): - shortestDist = math.inf - x, y = newPosition - for i, j in positions: - xdist = abs(i-x) - ydist = abs(j-y) - dist = math.sqrt(xdist**2+ydist**2) - if shortestDist > dist: shortestDist = dist - return shortestDist - - def drawMisses(self,guesses,word): - background = Image.new("RGBA",(600,400),color=(0,0,0,0)) - pos = [] - for guess in guesses: - if guess not in word: - placed = False - while placed == False: - letter = self.badText(guess,True) - w, h = self.FONT.getsize(guess) - x = random.randint(0,600-w) - y = random.randint(0,400-h) - if self.shortestDist(pos,(x,y)) > 70: - pos.append((x,y)) - background.paste(letter,(x,y),letter) - placed = True - return background - - def drawImage(self,channel): - self.bot.log("Drawing hangman image", channel) - game = self.bot.database["hangman games"].find_one({"_id":channel}) - - random.seed(game["game ID"]) - - background = Image.open("resources/paper.jpg") - try: - gallow = self.drawGallows() - except: - self.bot.log("Error drawing gallows (error code 1711)") - - try: - man = self.drawMan(game["misses"]) - except: - self.bot.log("Error drawing stick figure (error code 1712)") - - random.seed(game["game ID"]) - try: - letterLines = self.drawLetterLines(game["word"],game["guessed"],game["misses"]) - except: - self.bot.log("error drawing letter lines (error code 1713)") - - random.seed(game["game ID"]) - try: - misses = self.drawMisses(game["guessed letters"],game["word"]) - except: - self.bot.log("Error drawing misses (error code 1714)") - - background.paste(gallow,(100,100),gallow) - background.paste(man,(300,210),man) - background.paste(letterLines,(120,840),letterLines) - background.paste(misses,(600,150),misses) - - missesText = self.badText("MISSES",False) - missesTextWidth = missesText.size[0] - background.paste(missesText,(850-int(missesTextWidth/2),50),missesText) - - background.save("resources/games/hangmanBoards/hangmanBoard"+channel+".png") diff --git a/funcs/games/invest.py b/funcs/games/invest.py deleted file mode 100644 index 75bb33a..0000000 --- a/funcs/games/invest.py +++ /dev/null @@ -1,149 +0,0 @@ -import discord - -class Invest(): - def __init__(self, bot): - self.bot = bot - - def getPrice(self, symbol : str): - res = self.bot.finnhubClient.quote(symbol.upper()) - if res == {}: - return 0 - else: - return int(res["c"] * 100) - - def getPortfolio(self, user : str): - userInvestments = self.bot.database["investments"].find_one({"_id":user}) - - if userInvestments in [None,{}]: - return f"{self.bot.databaseFuncs.getName(user)} does not have a stock portfolio." - else: - portfolio = f"**Stock portfolio for {self.bot.databaseFuncs.getName(user)}**" - - for key, value in list(userInvestments["investments"].items()): - purchaseValue = value["purchased for"] - currentValue = int((self.getPrice(key) / value["value at purchase"]) * value["purchased"]) - if purchaseValue == "?": - portfolio += f"\n**{key}**: ___{str(currentValue)} GwendoBucks___" - else: - portfolio += f"\n**{key}**: ___{str(currentValue)} GwendoBucks___ (purchased for {str(purchaseValue)})" - - return portfolio - - def buyStock(self, user : str, stock : str, buyAmount : int): - if buyAmount >= 100: - if self.bot.money.checkBalance(user) >= buyAmount: - stockPrice = self.getPrice(stock) - if stockPrice > 0: - userInvestments = self.bot.database["investments"].find_one({"_id":user}) - - self.bot.money.addMoney(user,-1*buyAmount) - stock = stock.upper() - - if userInvestments != None: - userInvestments = userInvestments["investments"] - if stock in userInvestments: - value = userInvestments[stock] - newAmount = int((stockPrice / value["value at purchase"]) * value["purchased"]) + buyAmount - - self.bot.database["investments"].update_one({"_id":user}, - {"$set":{"investments."+stock+".value at purchase" : stockPrice}}) - - self.bot.database["investments"].update_one({"_id":user}, - {"$set":{"investments."+stock+".purchased" : newAmount}}) - - if value["purchased for"] != "?": - self.bot.database["investments"].update_one({"_id":user}, - {"$set":{"investments."+stock+".purchased for" : buyAmount}}) - else: - self.bot.database["investments"].update_one({"_id":user}, - {"$set":{"investments."+stock : {"purchased" : buyAmount, "value at purchase" : stockPrice, - "purchased for" : buyAmount}}}) - else: - newUser = {"_id":user,"investments":{stock : {"purchased" : buyAmount, "value at purchase" : stockPrice, "purchased for" : buyAmount}}} - self.bot.database["investments"].insert_one(newUser) - - return f"{self.bot.databaseFuncs.getName(user)} bought {buyAmount} GwendoBucks worth of {stock} stock" - else: - return f"{stock} is not traded on the american market." - else: - return "You don't have enough money for that" - else: - return "You cannot buy stocks for less than 100 GwendoBucks" - - def sellStock(self, user : str, stock : str, sellAmount : int): - if sellAmount > 0: - - userInvestments = self.bot.database["investments"].find_one({"_id":user})["investments"] - - stock = stock.upper() - - if userInvestments != None and stock in userInvestments: - value = userInvestments[stock] - stockPrice = self.getPrice(stock) - self.bot.database["investments"].update_one({"_id":user}, - {"$set":{"investments."+stock+".purchased" : - int((stockPrice / value["value at purchase"]) * value["purchased"])}}) - self.bot.database["investments"].update_one({"_id":user}, - {"$set":{"investments."+stock+".value at purchase" : stockPrice}}) - if value["purchased"] >= sellAmount: - self.bot.money.addMoney(user,sellAmount) - if sellAmount < value["purchased"]: - self.bot.database["investments"].update_one({"_id":user}, - {"$inc":{"investments."+stock+".purchased" : -sellAmount}}) - - self.bot.database["investments"].update_one({"_id":user}, - {"$set":{"investments."+stock+".purchased for" : "?"}}) - else: - self.bot.database["investments"].update_one({"_id":user}, - {"$unset":{"investments."+stock:""}}) - - return f"{self.bot.databaseFuncs.getName(user)} sold {sellAmount} GwendoBucks worth of {stock} stock" - else: - return f"You don't have enough {stock} stocks to do that" - else: - return f"You don't have any {stock} stock" - else: - return "no" - - async def parseInvest(self, ctx, parameters): - await self.bot.defer(ctx) - user = f"#{ctx.author.id}" - - if parameters.startswith("check"): - commands = parameters.split(" ") - if len(commands) == 1: - response = self.getPortfolio(user) - else: - price = self.getPrice(commands[1]) - if price == 0: - response = f"{commands[1].upper()} is not traded on the american market." - else: - price = f"{price:,}".replace(",",".") - response = f"The current {commands[1].upper()} stock is valued at **{price}** GwendoBucks" - - elif parameters.startswith("buy"): - commands = parameters.split(" ") - if len(commands) == 3: - response = self.buyStock(user,commands[1],int(commands[2])) - else: - response = "You must give both a stock name and an amount of gwendobucks you wish to spend." - - elif parameters.startswith("sell"): - commands = parameters.split(" ") - if len(commands) == 3: - try: - response = self.sellStock(user,commands[1],int(commands[2])) - except: - response = "The command must be given as \"/invest sell [stock] [amount of GwendoBucks to sell stocks for]\"" - else: - response = "You must give both a stock name and an amount of GwendoBucks you wish to sell stocks for." - - else: - response = "Incorrect parameters" - - if response.startswith("**"): - responses = response.split("\n") - em = discord.Embed(title=responses[0],description="\n".join(responses[1:]),colour=0x00FF00) - await ctx.send(embed=em) - else: - await ctx.send(response) diff --git a/funcs/games/money.py b/funcs/games/money.py deleted file mode 100644 index dea2812..0000000 --- a/funcs/games/money.py +++ /dev/null @@ -1,70 +0,0 @@ -class Money(): - - def __init__(self, bot): - self.bot = bot - self.database = bot.database - - # Returns the account balance for a user - def checkBalance(self, user): - self.bot.log("checking "+user+"'s account balance") - - userData = self.database["users"].find_one({"_id":user}) - - if userData != None: - return userData["money"] - else: return 0 - - async def sendBalance(self, ctx): - await self.bot.defer(ctx) - response = self.checkBalance("#"+str(ctx.author.id)) - if response == 1: - new_message = ctx.author.display_name + " has " + str(response) + " GwendoBuck" - else: - new_message = ctx.author.display_name + " has " + str(response) + " GwendoBucks" - await ctx.send(new_message) - - # Adds money to the account of a user - def addMoney(self,user,amount): - self.bot.log("adding "+str(amount)+" to "+user+"'s account") - - userData = self.database["users"].find_one({"_id":user}) - - if userData != None: - self.database["users"].update_one({"_id":user},{"$inc":{"money":amount}}) - else: - self.database["users"].insert_one({"_id":user,"user name":self.bot.databaseFuncs.getName(user),"money":amount}) - - # Transfers money from one user to another - async def giveMoney(self, ctx, user, amount): - await self.bot.defer(ctx) - username = user.display_name - if self.bot.databaseFuncs.getID(username) == None: - async for member in ctx.guild.fetch_members(limit=None): - if member.display_name.lower() == username.lower(): - username = member.display_name - userID = "#" + str(member.id) - newUser = {"_id":userID,"user name":username,"money":0} - self.bot.database["users"].insert_one(newUser) - - userData = self.database["users"].find_one({"_id":f"#{ctx.author.id}"}) - targetUser = self.bot.databaseFuncs.getID(username) - - if amount > 0: - if targetUser != None: - if userData != None: - if userData["money"] >= amount: - self.addMoney(f"#{ctx.author.id}",-1 * amount) - self.addMoney(targetUser,amount) - await ctx.send(f"Transferred {amount} GwendoBucks to {username}") - else: - self.bot.log("They didn't have enough GwendoBucks") - await ctx.send("You don't have that many GwendoBucks") - else: - self.bot.log("They didn't have enough GwendoBucks") - await ctx.send("You don't have that many GwendoBuck") - else: - self.bot.log("They weren't in the system") - await ctx.send("The target doesn't exist") - else: - self.bot.log("They tried to steal") - await ctx.send("Yeah, no. You can't do that") diff --git a/funcs/games/trivia.py b/funcs/games/trivia.py deleted file mode 100644 index eeb4003..0000000 --- a/funcs/games/trivia.py +++ /dev/null @@ -1,118 +0,0 @@ -import json -import urllib -import random -import asyncio - -class Trivia(): - def __init__(self, bot): - self.bot = bot - - # Starts a game of trivia. Downloads a question with answers, shuffles the wrong answers with the - # correct answer and returns the questions and answers. Also saves the question in the games.json file. - def triviaStart(self, channel : str): - question = self.bot.database["trivia questions"].find_one({"_id":channel}) - - self.bot.log("Trying to find a trivia question for "+channel) - - if question == None: - with urllib.request.urlopen("https://opentdb.com/api.php?amount=10&type=multiple") as response: - data = json.loads(response.read()) - - self.bot.log("Found the question \""+data["results"][0]["question"]+"\"") - answers = data["results"][0]["incorrect_answers"] - answers.append(data["results"][0]["correct_answer"]) - random.shuffle(answers) - correctAnswer = answers.index(data["results"][0]["correct_answer"]) + 97 - - self.bot.database["trivia questions"].insert_one({"_id":channel,"answer" : str(chr(correctAnswer)),"players" : {}}) - - replacements = {"'": "\'", - """: "\"", - "“": "\"", - "”": "\"", - "é": "é"} - question = data["results"][0]["question"] - - for key, value in replacements.items(): - question = question.replace(key,value) - - for answer in answers: - for key, value in replacements.items(): - answer = answer.replace(key,value) - - return question, answers, correctAnswer - else: - self.bot.log("There was already a trivia question for that channel (error code 1106)") - return "There's already a trivia question going on. Try again in like, a minute (error code 1106)", "", "" - - # Lets players answer a trivia question - def triviaAnswer(self, user : str, channel : str, command : str): - question = self.bot.database["trivia questions"].find_one({"_id":channel}) - - if command in ["a","b","c","d"]: - if question != None: - if user not in question["players"]: - self.bot.log(user+" answered the question in "+channel) - - self.bot.database["trivia questions"].update_one({"_id":channel},{"$set":{"players."+user : command}}) - - return "Locked in "+user+"'s answer" - else: - self.bot.log(user+" has already answered this question (error code 1105)") - return user+" has already answered this question (error code 1105)" - else: - self.bot.log("There's no question right now (error code 1104)") - return "There's no question right now (error code 1104)" - else: - self.bot.log("I didn't quite understand that (error code 1103)") - return "I didn't quite understand that (error code 1103)" - - - # Adds 1 GwendoBuck to each player that got the question right and deletes question from games.json. - def triviaCountPoints(self, channel : str): - question = self.bot.database["trivia questions"].find_one({"_id":channel}) - - self.bot.log("Counting points for question in "+channel) - - if question != None: - for player, answer in question["players"].items(): - if answer == question["answer"]: - self.bot.money.addMoney(player,1) - - - else: - self.bot.log("Couldn't find the question (error code 1102)") - - return None - - async def triviaParse(self, ctx, answer): - await self.bot.defer(ctx) - if answer == "": - question, options, correctAnswer = self.triviaStart(str(ctx.channel_id)) - if options != "": - results = "**"+question+"**\n" - for x, option in enumerate(options): - results += chr(x+97) + ") "+option+"\n" - - await ctx.send(results) - - await asyncio.sleep(60) - - self.triviaCountPoints(str(ctx.channel_id)) - - self.bot.databaseFuncs.deleteGame("trivia questions",str(ctx.channel_id)) - - self.bot.log("Time's up for the trivia question",str(ctx.channel_id)) - await ctx.send("Time's up The answer was \"*"+chr(correctAnswer)+") "+options[correctAnswer-97]+"*\". Anyone who answered that has gotten 1 GwendoBuck") - else: - await ctx.send(question, hidden=True) - - elif answer in ["a","b","c","d"]: - response = self.triviaAnswer("#"+str(ctx.author.id), str(ctx.channel_id), answer) - if response.startswith("Locked in "): - await ctx.send(f"{ctx.author.display_name} answered **{answer}**") - else: - await ctx.send(response) - else: - self.bot.log("I didn't understand that (error code 1101)",str(ctx.channel_id)) - await ctx.send("I didn't understand that (error code 1101)") diff --git a/funcs/other/bedreNetflix.py b/funcs/other/bedreNetflix.py deleted file mode 100644 index 5fb6c4a..0000000 --- a/funcs/other/bedreNetflix.py +++ /dev/null @@ -1,413 +0,0 @@ -import requests, imdb, discord, json, math, time, asyncio - -class BedreNetflix(): - def __init__(self,bot): - self.bot = bot - ip = ["localhost", "192.168.0.40"][self.bot.options.testing] - - self.radarrURL = "http://"+ip+":7878/api/v3/" - self.sonarrURL = "http://"+ip+":8989/api/" - self.qbittorrentURL = "http://"+ip+":8080/api/v2/" - self.moviePath = "/media/plex/Server/movies/" - self.showPath = "/media/plex/Server/Shows/" - - #Returns a list of no more than 5 options when user requests a movie - async def requestMovie(self, ctx, movieName): - await self.bot.defer(ctx) - - self.bot.log("Searching for "+movieName) - movieList = imdb.IMDb().search_movie(movieName) - movies = [] - for movie in movieList: - if movie["kind"] == "movie": - movies.append(movie) - if len(movies) > 5: - movies = movies[:5] - - if len(movies) == 1: - messageTitle = "**Is it this movie?**" - else: - messageTitle = "**Is it any of these movies?**" - - messageText = "" - imdbIds = [] - - for x, movie in enumerate(movies): - try: - messageText += "\n"+str(x+1)+") "+movie["title"]+" ("+str(movie["year"])+")" - except: - try: - messageText += "\n"+str(x+1)+") "+movie["title"] - except: - messageText += "Error" - imdbIds.append(movie.movieID) - - self.bot.log("Returning a list of "+str(len(movies))+" possible movies: "+str(imdbIds)) - - em = discord.Embed(title=messageTitle,description=messageText,colour=0x00FF00) - - message = await ctx.send(embed=em) - - messageData = {"messageID":message.id,"imdbIds":imdbIds} - - with open("resources/bedreNetflix/oldMessage"+str(ctx.channel.id),"w") as f: - json.dump(messageData,f) - - if len(movies) == 1: - await message.add_reaction("✔️") - else: - for x in range(len(movies)): - await message.add_reaction(["1️⃣","2️⃣","3️⃣","4️⃣","5️⃣"][x]) - - 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() - - #Adds the requested movie to Bedre Netflix - async def addMovie(self, message, imdbId, editMessage = True): - if imdbId == None: - self.bot.log("Did not find what the user was searching for") - if editMessage: - await message.edit(embed = None, content = "Try searching for the IMDB id") - else: - await message.channel.send("Try searching for the IMDB id") - else: - self.bot.log("Trying to add movie "+str(imdbId)) - apiKey = self.bot.credentials.radarrKey - response = requests.get(self.radarrURL+"movie/lookup/imdb?imdbId=tt"+imdbId+"&apiKey="+apiKey) - lookupData = response.json() - postData = {"qualityProfileId": 1, - "rootFolderPath" : self.moviePath, - "monitored" : True, - "addOptions": {"searchForMovie": True}} - for key in ["tmdbId","title","titleSlug","images","year"]: - postData.update({key : lookupData[key]}) - - r = requests.post(url= self.radarrURL+"movie?apikey="+apiKey,json = postData) - - if r.status_code == 201: - if editMessage: - await message.edit(embed = None, content = postData["title"]+" successfully added to Bedre Netflix") - else: - await message.channel.send(postData["title"]+" successfully added to Bedre Netflix") - - self.bot.log("Added "+postData["title"]+" to Bedre Netflix") - elif r.status_code == 400: - text = f"{postData['title']} is either already on Bedre Netflix, downloading, or not available" - if editMessage: - await message.edit(embed = None, content = text) - else: - await message.channel.send(text) - else: - if editMessage: - await message.edit(embed = None, content = "Something went wrong") - else: - await message.channel.send("Something went wrong") - self.bot.log(str(r.status_code)+" "+r.reason) - - #Returns a list of no more than 5 options when user requests a show - async def requestShow(self, ctx, showName): - await self.bot.defer(ctx) - - self.bot.log("Searching for "+showName) - movies = imdb.IMDb().search_movie(showName) #Replace with tvdb - shows = [] - for movie in movies: - if movie["kind"] in ["tv series","tv miniseries"]: - shows.append(movie) - if len(shows) > 5: - shows = shows[:5] - - if len(shows) == 1: - messageTitle = "**Is it this show?**" - else: - messageTitle = "**Is it any of these shows?**" - - messageText = "" - imdbNames = [] - - for x, show in enumerate(shows): - try: - messageText += "\n"+str(x+1)+") "+show["title"]+" ("+str(show["year"])+")" - except: - try: - messageText += "\n"+str(x+1)+") "+show["title"] - except: - messageText += "Error" - imdbNames.append(show["title"]) - - self.bot.log("Returning a list of "+str(len(shows))+" possible shows: "+str(imdbNames)) - - em = discord.Embed(title=messageTitle,description=messageText,colour=0x00FF00) - - message = await ctx.send(embed=em) - - messageData = {"messageID":message.id,"imdbNames":imdbNames} - - with open("resources/bedreNetflix/oldMessage"+str(ctx.channel.id),"w") as f: - json.dump(messageData,f) - - if len(shows) == 1: - await message.add_reaction("✔️") - else: - for x in range(len(shows)): - await message.add_reaction(["1️⃣","2️⃣","3️⃣","4️⃣","5️⃣"][x]) - - 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() - - #Adds the requested show to Bedre Netflix - async def addShow(self, message, imdbName): - if imdbName == 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") - else: - self.bot.log("Trying to add show "+str(imdbName)) - apiKey = self.bot.credentials.sonarrKey - response = requests.get(self.sonarrURL+"series/lookup?term="+imdbName.replace(" ","%20")+"&apiKey="+apiKey) - lookupData = response.json()[0] - postData = {"ProfileId" : 1, - "rootFolderPath" : self.showPath, - "monitored" : True, - "addOptions" : {"searchForMissingEpisodes" : True}} - for key in ["tvdbId","title","titleSlug","images","seasons"]: - postData.update({key : lookupData[key]}) - - r = requests.post(url= self.sonarrURL+"series?apikey="+apiKey,json = postData) - - if r.status_code == 201: - await message.edit(embed = None, content = postData["title"]+" successfully added to Bedre Netflix") - self.bot.log("Added a "+postData["title"]+" to Bedre Netflix") - elif r.status_code == 400: - text = f"{postData['title']} is either already on Bedre Netflix, downloading, or not available" - await message.edit(embed = None, content = text) - else: - await message.edit(embed = None, content = "Something went wrong") - self.bot.log(str(r.status_code)+" "+r.reason) - - #Generates a list of all torrents and returns formatted list and whether all torrents are downloaded - async def genDownloadList(self, showDM, showMovies, showShows, episodes): - self.bot.log("Generating torrent list") - titleWidth = 100 - message = [] - allDownloaded = True - - if showDM: - message.append("") - DMSectionTitle = "*Torrent Downloads*" - DMSectionTitleLine = "-"*((titleWidth-len(DMSectionTitle))//2) - message.append(DMSectionTitleLine+DMSectionTitle+DMSectionTitleLine) - response = requests.get(self.qbittorrentURL+"torrents/info") - torrentList = response.json() - - if len(torrentList) > 0: - for torrent in torrentList: - torrentName = torrent["name"] - if len(torrentName) > 30: - if torrentName[26] == " ": - torrentName = torrentName[:26]+"...." - else: - torrentName = torrentName[:27]+"..." - while len(torrentName) < 30: - torrentName += " " - - if torrent["size"] == 0: - downloadedRatio = 0 - elif torrent["amount_left"] == 0: - downloadedRatio = 1 - else: - downloadedRatio = min(torrent["downloaded"]/torrent["size"],1) - progressBar = "|"+("█"*math.floor(downloadedRatio*20)) - while len(progressBar) < 21: - progressBar += " " - - progressBar += "| "+str(math.floor(downloadedRatio*100))+"%" - - while len(progressBar) < 27: - progressBar += " " - - etaInSeconds = torrent["eta"] - - if etaInSeconds >= 8640000: - eta = "∞" - else: - eta = "" - if etaInSeconds >= 86400: - eta += str(math.floor(etaInSeconds/86400))+"d " - if etaInSeconds >= 3600: - eta += str(math.floor((etaInSeconds%86400)/3600))+"h " - if etaInSeconds >= 60: - eta += str(math.floor((etaInSeconds%3600)/60))+"m " - - eta += str(etaInSeconds%60)+"s" - - torrentInfo = torrentName+" "+progressBar+" (Eta: "+eta+")" - - if torrent["state"] == "stalledDL": - torrentInfo += " (Stalled)" - - if not (downloadedRatio == 1 and torrent["last_activity"] < time.time()-7200): - message.append(torrentInfo) - - if downloadedRatio < 1 and torrent["state"] != "stalledDL": - allDownloaded = False - else: - message.append("No torrents currently downloading") - - if showMovies: - message.append("") - movieSectionTitle = "*Missing movies not downloading*" - movieSectionTitleLine = "-"*((titleWidth-len(movieSectionTitle))//2) - message.append(movieSectionTitleLine+movieSectionTitle+movieSectionTitleLine) - movieList = requests.get(self.radarrURL+"movie?apiKey="+self.bot.credentials.radarrKey).json() - movieQueue = requests.get(self.radarrURL+"queue?apiKey="+self.bot.credentials.radarrKey).json() - movieQueueIDs = [] - - for queueItem in movieQueue["records"]: - movieQueueIDs.append(queueItem["movieId"]) - - for movie in movieList: - if not movie["hasFile"]: - if movie["id"] not in movieQueueIDs: - movieName = movie["title"] - if len(movieName) > 40: - if movieName[36] == " ": - movieName = movieName[:36]+"...." - else: - movieName = movieName[:37]+"..." - - while len(movieName) < 41: - movieName += " " - - if movie["monitored"]: - movieInfo = movieName+"Could not find a torrent" - else: - movieInfo = movieName+"No torrent exists. Likely because the movie is not yet released on DVD" - - message.append(movieInfo) - - if showShows: - message.append("") - showSectionTitle = "*Missing shows not downloading*" - showSectionTitleLine = "-"*((titleWidth-len(showSectionTitle))//2) - message.append(showSectionTitleLine+showSectionTitle+showSectionTitleLine) - - showList = requests.get(self.sonarrURL+"series?apiKey="+self.bot.credentials.sonarrKey).json() - - for show in showList: - if show["seasons"][0]["seasonNumber"] == 0: - seasons = show["seasons"][1:] - else: - seasons = show["seasons"] - if any(i["statistics"]["episodeCount"] != i["statistics"]["totalEpisodeCount"] for i in seasons): - if all(i["statistics"]["episodeCount"] == 0 for i in seasons): - message.append(show["title"] + " (all episodes)") - else: - if episodes: - missingEpisodes = sum(i["statistics"]["totalEpisodeCount"] - i["statistics"]["episodeCount"] for i in seasons) - message.append(show["title"] + f" ({missingEpisodes} episodes)") - - message.append("-"*titleWidth) - - messageText = "```"+"\n".join(message[1:])+"```" - if messageText == "``````": - messageText = "There are no torrents downloading right. If the torrent you're looking for was added more than 24 hours ago, it might already be on Bedre Netflix." - return messageText, allDownloaded - - async def downloading(self, ctx, content): - async def SendLongMessage(ctx,messageText): - if len(messageText) <= 1994: - await ctx.send("```"+messageText+"```") - else: - cutOffIndex = messageText[:1994].rfind("\n") - await ctx.send("```"+messageText[:cutOffIndex]+"```") - await SendLongMessage(ctx,messageText[cutOffIndex+1:]) - - await self.bot.defer(ctx) - - # showDM, showMovies, showShows, episodes - params = [False, False, False, False] - showDMArgs = ["d", "dm", "downloading", "downloadmanager"] - showMoviesArgs = ["m", "movies"] - showShowsArgs = ["s", "shows", "series"] - episodesArgs = ["e", "episodes"] - argList = [showDMArgs, showMoviesArgs, showShowsArgs, episodesArgs] - inputArgs = [] - validArguments = True - - while content != "" and validArguments: - if content[0] == " ": - content = content[1:] - elif content[0] == "-": - if content[1] == "-": - argStart = 2 - if " " in content: - argStop = content.find(" ") - else: - argStop = None - else: - argStart = 1 - argStop = 2 - - inputArgs.append(content[argStart:argStop]) - if argStop is None: - content = "" - else: - content = content[argStop:] - else: - validArguments = False - - if validArguments: - for x, argAliases in enumerate(argList): - argInInput = [i in inputArgs for i in argAliases] - if any(argInInput): - inputArgs.remove(argAliases[argInInput.index(True)]) - params[x] = True - - if len(inputArgs) != 0 or (params[2] == False and params[3] == True): - validArguments = False - - showAnything = any(i for i in params) - if validArguments and showAnything: - messageText, allDownloaded = await self.genDownloadList(*params) - if messageText.startswith("```"): - - if len(messageText) <= 2000: - if not allDownloaded: - updatesLeft = 60 - messageText = messageText[:-3]+"\nThis message will update every 10 seconds for "+str(math.ceil(updatesLeft/6))+" more minutes\n```" - oldMessage = await ctx.send(messageText) - - while ((not allDownloaded) and updatesLeft > 0): - await asyncio.sleep(10) - updatesLeft -= 1 - messageText, allDownloaded = await self.genDownloadList(*params) - messageText = messageText[:-3]+"\nThis message will update every 10 seconds for "+str(math.ceil(updatesLeft/6))+" more minutes\n```" - await oldMessage.edit(content = messageText) - - messageText, allDownloaded = await self.genDownloadList(*params) - - if messageText.startswith("```"): - if allDownloaded: - self.bot.log("All torrents are downloaded") - else: - messageText = messageText[:-3]+"\nThis message will not update anymore\n```" - self.bot.log("The message updated 20 times") - - await oldMessage.edit(content = messageText) - - else: - await ctx.send(messageText) - else: - messageText = messageText[3:-3] - await SendLongMessage(ctx,messageText) - else: - await ctx.send(messageText) - else: - await ctx.send("Invalid or repeated parameters. Use '/help downloading' to see valid parameters.") - diff --git a/gwendolyn/__init__.py b/gwendolyn/__init__.py new file mode 100644 index 0000000..17b7d0d --- /dev/null +++ b/gwendolyn/__init__.py @@ -0,0 +1,6 @@ +"""The main module for Gwendolyn.""" +# pylint: disable=invalid-name + +__all__ = ["funcs", "utils", "Gwendolyn"] + +from .gwendolyn_client import Gwendolyn diff --git a/gwendolyn/cogs/event_cog.py b/gwendolyn/cogs/event_cog.py new file mode 100644 index 0000000..4737f8c --- /dev/null +++ b/gwendolyn/cogs/event_cog.py @@ -0,0 +1,40 @@ +"""Contains the EventCog, which runs code for specific bot events.""" +from discord.ext import commands # Has the cog class + + +class EventCog(commands.Cog): + """Handles bot events.""" + + def __init__(self, bot): + """Initialize the bot.""" + self.bot = bot + self.bot.on_error = self.on_error + + @commands.Cog.listener() + async def on_ready(self): + """Log and set bot status when bot logs in.""" + await self.bot.event_handler.on_ready() + + @commands.Cog.listener() + async def on_slash_command(self, ctx): + """Log when a slash command is run.""" + await self.bot.event_handler.on_slash_command(ctx) + + @commands.Cog.listener() + async def on_slash_command_error(self, ctx, error): + """Log when a slash error occurs.""" + await self.bot.error_handler.on_slash_command_error(ctx, error) + + async def on_error(self, method, *args, **kwargs): # pylint: disable=unused-argument + """Log when an error occurs.""" + await self.bot.error_handler.on_error(method) + + @commands.Cog.listener() + async def on_reaction_add(self, reaction, user): + """Handle when someone reacts to a message.""" + await self.bot.event_handler.on_reaction_add(reaction, user) + + +def setup(bot): + """Add the eventcog to the bot.""" + bot.add_cog(EventCog(bot)) diff --git a/gwendolyn/cogs/game_cog.py b/gwendolyn/cogs/game_cog.py new file mode 100644 index 0000000..8b04fd9 --- /dev/null +++ b/gwendolyn/cogs/game_cog.py @@ -0,0 +1,177 @@ +"""Contains all the cogs that deal with game commands.""" +from discord.ext import commands # Has the cog class +from discord_slash import cog_ext # Used for slash commands + +from gwendolyn.utils import get_params # pylint: disable=import-error + +params = get_params() + + +class GamesCog(commands.Cog): + """Contains miscellaneous game commands.""" + + def __init__(self, bot): + """Initialize the cog.""" + self.bot = bot + + @cog_ext.cog_slash(**params["balance"]) + async def balance(self, ctx): + """Check user balance.""" + await self.bot.money.sendBalance(ctx) + + @cog_ext.cog_slash(**params["give"]) + async def give(self, ctx, user, amount): + """Give another user an amount of GwendoBucks.""" + await self.bot.money.giveMoney(ctx, user, amount) + + @cog_ext.cog_slash(**params["invest"]) + async def invest(self, ctx, parameters="check"): + """Invest GwendoBucks in the stock market.""" + await self.bot.games.invest.parseInvest(ctx, parameters) + + @cog_ext.cog_slash(**params["trivia"]) + async def trivia(self, ctx, answer=""): + """Run a game of trivia.""" + await self.bot.games.trivia.triviaParse(ctx, answer) + + +class BlackjackCog(commands.Cog): + """Contains the blackjack commands.""" + + def __init__(self, bot): + """Initialize the cog.""" + self.bot = bot + + @cog_ext.cog_subcommand(**params["blackjack_start"]) + async def blackjack_start(self, ctx): + """Start a game of blackjack.""" + await self.bot.games.blackjack.start(ctx) + + @cog_ext.cog_subcommand(**params["blackjack_bet"]) + async def blackjack_bet(self, ctx, bet): + """Enter the game of blackjack with a bet.""" + await self.bot.games.blackjack.enter_game(ctx, bet) + + @cog_ext.cog_subcommand(**params["blackjack_stand"]) + async def blackjack_stand(self, ctx, hand=""): + """Stand on your hand in blackjack.""" + await self.bot.games.blackjack.stand(ctx, hand) + + @cog_ext.cog_subcommand(**params["blackjack_hit"]) + async def blackjack_hit(self, ctx, hand=0): + """Hit on your hand in blackjack.""" + await self.bot.games.blackjack.hit(ctx, hand) + + @cog_ext.cog_subcommand(**params["blackjack_double"]) + async def blackjack_double(self, ctx, hand=0): + """Double in blackjack.""" + await self.bot.games.blackjack.double(ctx, hand) + + @cog_ext.cog_subcommand(**params["blackjack_split"]) + async def blackjack_split(self, ctx, hand=0): + """Split your hand in blackjack.""" + await self.bot.games.blackjack.split(ctx, hand) + + @cog_ext.cog_subcommand(**params["blackjack_hilo"]) + async def blackjack_hilo(self, ctx): + """Get the hilo value for the deck in blackjack.""" + await self.bot.games.blackjack.hilo(ctx) + + @cog_ext.cog_subcommand(**params["blackjack_shuffle"]) + async def blackjack_shuffle(self, ctx): + """Shuffle the blackjack game.""" + await self.bot.games.blackjack.shuffle(ctx) + + @cog_ext.cog_subcommand(**params["blackjack_cards"]) + async def blackjack_cards(self, ctx): + """Get the amount of cards left in the blackjack deck.""" + await self.bot.games.blackjack.cards(ctx) + + +class ConnectFourCog(commands.Cog): + """Contains all the connect four commands.""" + + def __init__(self, bot): + """Initialize the cog.""" + self.bot = bot + + @cog_ext.cog_subcommand(**params["connect_four_start_user"]) + async def connect_four_start_user(self, ctx, user): + """Start a game of connect four against another user.""" + await self.bot.games.connect_four.start(ctx, user) + + @cog_ext.cog_subcommand(**params["connect_four_start_gwendolyn"]) + async def connect_four_start_gwendolyn(self, ctx, difficulty=3): + """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.""" + + def __init__(self, bot): + """Initialize the cog.""" + self.bot = bot + + @cog_ext.cog_subcommand(**params["hangman_start"]) + async def hangman_start(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.""" + + def __init__(self, bot): + """Initialize the cog.""" + self.bot = bot + self.hex = self.bot.games.hex + + @cog_ext.cog_subcommand(**params["hex_start_user"]) + async def hex_start_user(self, ctx, user): + """Start a game of hex against another player.""" + await self.hex.start(ctx, user) + + @cog_ext.cog_subcommand(**params["hex_start_gwendolyn"]) + async def hex_start_gwendolyn(self, ctx, difficulty=2): + """Start a game of hex against Gwendolyn.""" + await self.hex.start(ctx, difficulty) + + @cog_ext.cog_subcommand(**params["hex_place"]) + async def hex_place(self, ctx, coordinates): + """Place a piece in the hex game.""" + await self.hex.placeHex(ctx, coordinates, f"#{ctx.author.id}") + + @cog_ext.cog_subcommand(**params["hex_undo"]) + async def hex_undo(self, ctx): + """Undo your last hex move.""" + await self.hex.undo(ctx) + + @cog_ext.cog_subcommand(**params["hex_swap"]) + async def hex_swap(self, ctx): + """Perform a hex swap.""" + await self.hex.swap(ctx) + + @cog_ext.cog_subcommand(**params["hex_surrender"]) + async def hex_surrender(self, ctx): + """Surrender the hex game.""" + await self.hex.surrender(ctx) + + +def setup(bot): + """Add all the cogs to the bot.""" + bot.add_cog(GamesCog(bot)) + bot.add_cog(BlackjackCog(bot)) + bot.add_cog(ConnectFourCog(bot)) + bot.add_cog(HangmanCog(bot)) + bot.add_cog(HexCog(bot)) diff --git a/gwendolyn/cogs/lookup_cog.py b/gwendolyn/cogs/lookup_cog.py new file mode 100644 index 0000000..77c1114 --- /dev/null +++ b/gwendolyn/cogs/lookup_cog.py @@ -0,0 +1,32 @@ +"""Contains the LookupCog, which deals with the lookup commands.""" +from discord.ext import commands # Has the cog class +from discord_slash import cog_ext # Used for slash commands + +from gwendolyn.utils import get_params # pylint: disable=import-error + +params = get_params() + + +class LookupCog(commands.Cog): + """Contains the lookup commands.""" + + def __init__(self, bot): + """Initialize the cog.""" + self.bot = bot + + # Looks up a spell + @cog_ext.cog_slash(**params["spell"]) + async def spell(self, ctx, query): + """Look up a spell.""" + await self.bot.lookup_funcs.spellFunc(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) + + +def setup(bot): + """Add the cog to the bot.""" + bot.add_cog(LookupCog(bot)) diff --git a/gwendolyn/cogs/misc_cog.py b/gwendolyn/cogs/misc_cog.py new file mode 100644 index 0000000..5042757 --- /dev/null +++ b/gwendolyn/cogs/misc_cog.py @@ -0,0 +1,99 @@ +"""Contains the MiscCog, which deals with miscellaneous commands.""" +from discord.ext import commands # Has the cog class +from discord_slash import cog_ext # Used for slash commands + +from gwendolyn.utils import get_params # pylint: disable=import-error + +params = get_params() + + +class MiscCog(commands.Cog): + """Contains the miscellaneous commands.""" + + def __init__(self, bot): + """Initialize the cog.""" + self.bot = bot + self.bot.remove_command("help") + self.generators = bot.other.generators + self.plex = bot.other.plex + self.nerd_shit = bot.other.nerd_shit + + @cog_ext.cog_slash(**params["ping"]) + async def ping(self, ctx): + """Send the bot's latency.""" + await ctx.send(f"Pong!\nLatency is {round(self.bot.latency * 1000)}ms") + + @cog_ext.cog_slash(**params["stop"]) + async def stop(self, ctx): + """Stop the bot.""" + await self.bot.stop(ctx) + + @cog_ext.cog_slash(**params["help"]) + async def help_command(self, ctx, command=""): + """Get help for commands.""" + await self.bot.other.helpFunc(ctx, command) + + @cog_ext.cog_slash(**params["thank"]) + async def thank(self, ctx): + """Thank the bot.""" + await ctx.send("You're welcome :blush:") + + @cog_ext.cog_slash(**params["hello"]) + async def hello(self, ctx): + """Greet the bot.""" + await self.bot.other.helloFunc(ctx) + + @cog_ext.cog_slash(**params["roll"]) + async def roll(self, ctx, dice="1d20"): + """Roll dice.""" + await self.bot.other.rollDice(ctx, dice) + + @cog_ext.cog_slash(**params["image"]) + async def image(self, ctx): + """Get a random image from Bing.""" + await self.bot.other.imageFunc(ctx) + + @cog_ext.cog_slash(**params["movie"]) + async def movie(self, ctx): + """Get a random movie from the Plex server.""" + await self.bot.other.movieFunc(ctx) + + @cog_ext.cog_slash(**params["name"]) + async def name(self, ctx): + """Generate a random name.""" + await self.generators.nameGen(ctx) + + @cog_ext.cog_slash(**params["tavern"]) + async def tavern(self, ctx): + """Generate a random tavern name.""" + await self.generators.tavernGen(ctx) + + @cog_ext.cog_slash(**params["wiki"]) + async def wiki(self, ctx, wiki_page=""): + """Get a page on a fandom wiki.""" + await self.bot.other.findWikiPage(ctx, wiki_page) + + @cog_ext.cog_slash(**params["add_movie"]) + async def add_movie(self, ctx, movie): + """Search for a movie and add it to the Plex server.""" + await self.plex.request_movie(ctx, movie) + + @cog_ext.cog_slash(**params["add_show"]) + async def add_show(self, ctx, show): + """Search for a show and add it to the Plex server.""" + await self.plex.request_show(ctx, show) + + @cog_ext.cog_slash(**params["downloading"]) + async def downloading(self, ctx, parameters="-d"): + """Get the current downloading torrents.""" + await self.plex.downloading(ctx, parameters) + + @cog_ext.cog_slash(**params["wolf"]) + async def wolf(self, ctx, query): + """Perform a search on Wolfram Alpha.""" + await self.nerd_shit.wolfSearch(ctx, query) + + +def setup(bot): + """Add the cog to the bot.""" + bot.add_cog(MiscCog(bot)) diff --git a/gwendolyn/cogs/star_wars_cog.py b/gwendolyn/cogs/star_wars_cog.py new file mode 100644 index 0000000..10101f0 --- /dev/null +++ b/gwendolyn/cogs/star_wars_cog.py @@ -0,0 +1,40 @@ +"""Contains the StarWarsCog, which deals with Star Wars commands.""" +from discord.ext import commands +from discord_slash import cog_ext + +from gwendolyn.utils import get_params # pylint: disable=import-error + +params = get_params() + + +class StarWarsCog(commands.Cog): + """Contains the Star Wars commands.""" + + def __init__(self, bot): + """Initialize the cog.""" + self.bot = bot + + @cog_ext.cog_slash(**params["star_wars_roll"]) + async def star_wars_roll(self, ctx, dice=""): + """Roll Star Wars dice.""" + await self.bot.star_wars.roll.parseRoll(ctx, dice) + + @cog_ext.cog_slash(**params["star_wars_destiny"]) + async def star_wars_destiny(self, ctx, parameters=""): + """Control Star Wars destiny points.""" + await self.bot.star_wars.destiny.parseDestiny(ctx, parameters) + + @cog_ext.cog_slash(**params["star_wars_crit"]) + async def star_wars_crit(self, ctx, severity: int = 0): + """Roll for critical injuries.""" + await self.bot.star_wars.roll.critRoll(ctx, severity) + + @cog_ext.cog_slash(**params["star_wars_character"]) + async def star_wars_character(self, ctx, parameters=""): + """Access and change Star Wars character sheet data.""" + await self.bot.star_wars.character.parseChar(ctx, parameters) + + +def setup(bot): + """Add the cog to the bot.""" + bot.add_cog(StarWarsCog(bot)) diff --git a/funcs/__init__.py b/gwendolyn/funcs/__init__.py similarity index 59% rename from funcs/__init__.py rename to gwendolyn/funcs/__init__.py index 729f445..a8c7fc8 100644 --- a/funcs/__init__.py +++ b/gwendolyn/funcs/__init__.py @@ -1,6 +1,6 @@ """A collection of all Gwendolyn functions.""" -__all__ = ["Games" , "Money", "LookupFuncs", "StarWars"] +__all__ = ["Games", "Money", "LookupFuncs", "StarWars"] from .games import Money, Games @@ -8,4 +8,4 @@ from .lookup import LookupFuncs from .other import Other -from .starWarsFuncs import StarWars +from .star_wars_funcs import StarWars diff --git a/funcs/games/__init__.py b/gwendolyn/funcs/games/__init__.py similarity index 74% rename from funcs/games/__init__.py rename to gwendolyn/funcs/games/__init__.py index 8d19ead..9eae213 100644 --- a/funcs/games/__init__.py +++ b/gwendolyn/funcs/games/__init__.py @@ -3,4 +3,4 @@ __all__ = ["Money", "Games"] from .money import Money -from .gamesContainer import Games +from .games_container import Games diff --git a/gwendolyn/funcs/games/blackjack.py b/gwendolyn/funcs/games/blackjack.py new file mode 100644 index 0000000..57c053a --- /dev/null +++ b/gwendolyn/funcs/games/blackjack.py @@ -0,0 +1,1499 @@ +""" +Runs commands, game logic and imaging for blackjack games. + +*Classes* +--------- + Blackjack + Contains the blackjack game logic. + DrawBlackjack + Draws images of the blackjack table. +""" +import random # Used to shuffle the blackjack cards +import math # Used for flooring decimal numbers +import datetime # Used to generate the game id +import asyncio # Used for sleeping +from shutil import copyfile + +import discord # Used for discord.file +import discord_slash # Used for typehints +from PIL import Image, ImageDraw, ImageFont + +from gwendolyn.utils import replace_multiple + + +class Blackjack(): + """ + Deals with blackjack commands and gameplay logic. + + *Methods* + --------- + hit(ctx: discord_slash.context.SlashContext, + hand_number: int = 0) + double(ctx: discord_slash.context.SlashContext, + hand_number: int = 0) + stand(ctx: discord_slash.context.SlashContext, + hand_number: int = 0) + split(ctx: discord_slash.context.SlashContext, + hand_number: int = 0) + enter_game(ctx: discord_slash.context.SlashContext, bet: int) + start(ctx: discord_slash.context.SlashContext) + hilo(ctx: discord_slash.context.SlashContext) + shuffle(ctx: discord_slash.context.SlashContext) + cards(ctx: discord_slash.context.SlashContext) + """ + + def __init__(self, bot): + """Initialize the class.""" + self.bot = bot + self.draw = DrawBlackjack(bot) + self.decks = 4 + self.long_strings = self.bot.long_strings + + def _blackjack_shuffle(self, channel: str): + """ + Shuffle an amount of decks equal to self.decks. + + The shuffled cards are placed in the database as the cards that + are used by other blackjack functions. + + *Parameters* + ------------ + channel: str + The id of the channel where the cards will be used. + """ + self.bot.log("Shuffling the blackjack deck") + + deck_path = "gwendolyn/resources/games/deck_of_cards.txt" + with open(deck_path, "r") as file_pointer: + deck = file_pointer.read() + + all_decks = deck.split("\n") * self.decks + random.shuffle(all_decks) + + blackjack_cards = self.bot.database["blackjack cards"] + cards = {"_id": channel} + card_updater = {"$set": {"_id": channel, "cards": all_decks}} + blackjack_cards.update_one(cards, card_updater, upsert=True) + + # Creates hilo file + self.bot.log(f"creating hilo doc for {channel}") + data = 0 + blackjack_hilo = self.bot.database["hilo"] + hilo_updater = {"$set": {"_id": channel, "hilo": data}} + blackjack_hilo.update_one({"_id": channel}, hilo_updater, upsert=True) + + def _calc_hand_value(self, hand: list): + """ + Calculate the value of a blackjack hand. + + *Parameters* + ------------ + hand: list + The hand to calculate the value of. Each element in the + list is a card represented as a string in the format of + "xy" where x is the number of the card (0, k, q, and j + for 10s, kings, queens and jacks) and y is the suit (d, + c, h or s). + + *Returns* + --------- + hand_value: int + The blackjack value of the hand. + """ + values = [0] + + for card in hand: + card_value = replace_multiple(card[0], ["0", "k", "q", "j"], "10") + if card_value == "a": + length = len(values) + for i in range(length): + values.append(values[i] + 11) + values[i] += 1 + else: + values = [int(i)+int(card_value) for i in values] + + values.sort() + + hand_value = values[0] + for value in values: + if value <= 21: + hand_value = value + + self.bot.log(f"Calculated the value of {hand} to be {hand_value}") + + return hand_value + + def _draw_card(self, channel: str): + """ + Draw a card from the stack. + + *Parameters* + ------------ + channel: str + The id of the channel the card is drawn in. + """ + self.bot.log("drawing a card") + + blackjack_cards = self.bot.database["blackjack cards"] + drawn_card = blackjack_cards.find_one({"_id": channel})["cards"][0] + blackjack_cards.update_one({"_id": channel}, {"$pop": {"cards": -1}}) + value = self._calc_hand_value([drawn_card]) + + blackjack_hilo = self.bot.database["hilo"] + + if value <= 6: + blackjack_hilo.update_one({"_id": channel}, {"$inc": {"hilo": 1}}) + elif value >= 10: + blackjack_hilo.update_one({"_id": channel}, {"$inc": {"hilo": -1}}) + + return drawn_card + + def _dealer_draw(self, channel: str): + """ + Draw a card for the dealer. + + *Parameters* + ------------ + channel: str + The id of the channel to draw a card for the dealer in. + + *Returns* + --------- + done: bool + Whether the dealer is done drawing cards. + """ + game = self.bot.database["blackjack games"].find_one({"_id": channel}) + + done = False + dealer_hand = game["dealer hand"] + + blackjack_games = self.bot.database["blackjack games"] + + if self._calc_hand_value(dealer_hand) < 17: + dealer_hand.append(self._draw_card(channel)) + dealer_updater = {"$set": {"dealer hand": dealer_hand}} + blackjack_games.update_one({"_id": channel}, dealer_updater) + else: + done = True + + if self._calc_hand_value(dealer_hand) > 21: + dealer_updater = {"$set": {"dealer busted": True}} + blackjack_games.update_one({"_id": channel}, dealer_updater) + + return done + + def _blackjack_continue(self, channel: str): + """ + Continues the blackjack game to the next round. + + *Parameters* + ------------ + channel: str + The id of the channel the blackjack game is in + + *Returns* + --------- + send_message: str + The message to send to the channel. + all_standing: bool + If all players are standing. + gameDone: bool + If the game has finished. + """ + self.bot.log("Continuing blackjack game") + game = self.bot.database["blackjack games"].find_one({"_id": channel}) + + done = False + + blackjack_games = self.bot.database["blackjack games"] + blackjack_games.update_one({"_id": channel}, {"$inc": {"round": 1}}) + + all_standing = True + pre_all_standing = True + message = self.long_strings["Blackjack all players standing"] + + if game["all standing"]: + self.bot.log("All are standing") + + done = self._dealer_draw(channel) + message = "The dealer draws a card." + + blackjack_games.find_one({"_id": channel}) + + self.bot.log("Testing if all are standing") + for user in game["user hands"]: + user_hand = game["user hands"][user] + test_parameters = [user_hand, all_standing, pre_all_standing, True] + standing_test = (self._test_if_standing(*test_parameters)) + new_user, all_standing, pre_all_standing = standing_test + hand_updater = {"$set": {"user hands."+user: new_user}} + blackjack_games.update_one({"_id": channel}, hand_updater) + + if all_standing: + game_updater = {"$set": {"all standing": True}} + blackjack_games.update_one({"_id": channel}, game_updater) + + self.draw.draw_image(channel) + + if all_standing: + if not done: + return message, True, done + else: + return "The dealer is done drawing cards", True, done + elif pre_all_standing: + return "", True, done + else: + send_message = self.long_strings["Blackjack commands"] + + if game["round"] == 0: + send_message = send_message.format( + self.long_strings["Blackjack first round"]) + else: + send_message = send_message.format("") + + return send_message, False, done + + def _test_if_standing(self, hand: dict, all_standing: bool, + pre_all_standing: bool, top_level: bool): + """ + Test if a player is standing on all their hands. + + Also resets the hand if it's not standing + + *Parameters* + ------------ + hand: dict + The hand to test and reset. + all_standing: bool + Is set to True at the top level. If it's false, the + player is not standing on one of the previously tested + hands. + pre_all_standing: bool + Is set to True at the top level. + top_level: bool + If the input hand is _all_ if the player's hands. If + False, it's one of the hands resulting from a split. + + *Returns* + --------- + hand: dict + The reset hand. + all_standing: bool + If the player is standing on all their hands. + pre_all_standing: bool + Is true if all_standing is True, or if a player has done + something equivalent to standing but still needs to see + the newly drawn card. + """ + # If the user has not hit, they are by definition standing. + if not hand["hit"]: + hand["standing"] = True + + if not hand["standing"]: + all_standing = False + + if self._calc_hand_value(hand["hand"]) >= 21 or hand["doubled"]: + hand["standing"] = True + else: + pre_all_standing = False + + hand["hit"] = False + + if top_level: + if hand["split"] >= 1: + test_hand = hand["other hand"] + test_parameters = [ + test_hand, + all_standing, + pre_all_standing, + False + ] + standing_test = (self._test_if_standing(*test_parameters)) + hand["other hand"] = standing_test[0] + all_standing = standing_test[1] + pre_all_standing = standing_test[2] + if hand["split"] >= 2: + test_hand = hand["third hand"] + test_parameters = [ + test_hand, + all_standing, + pre_all_standing, + False + ] + standing_test = (self._test_if_standing(*test_parameters)) + hand["third hand"] = standing_test[0] + all_standing = standing_test[1] + pre_all_standing = standing_test[2] + if hand["split"] >= 3: + test_hand = hand["fourth hand"] + test_parameters = [ + test_hand, + all_standing, + pre_all_standing, + False + ] + standing_test = (self._test_if_standing(*test_parameters)) + hand["fourth hand"] = standing_test[0] + all_standing = standing_test[1] + pre_all_standing = standing_test[2] + + return hand, all_standing, pre_all_standing + + def _blackjack_finish(self, channel: str): + """ + Generate the winnings message after the blackjack game ends. + + *Parameters* + ------------ + channel: str + The id of the channel the game is in. + + *Returns* + --------- + final_winnings: str + The winnings message. + """ + final_winnings = "*Final Winnings:*\n" + + game = self.bot.database["blackjack games"].find_one({"_id": channel}) + + dealer_value = self._calc_hand_value(game["dealer hand"]) + dealer_blackjack = game["dealer blackjack"] + dealer_busted = game["dealer busted"] + + for user in game["user hands"]: + calc_winnings_parameters = [ + game["user hands"][user], + dealer_value, + True, + dealer_blackjack, + dealer_busted + ] + winnings_calc = (self._calc_winning(*calc_winnings_parameters)) + winnings, net_winnings, reason = winnings_calc + + user_name = self.bot.database_funcs.get_name(user) + + if winnings < 0: + if winnings == -1: + final_winnings += "{} lost 1 GwendoBuck {}\n".format( + user_name, + reason + ) + else: + money_lost = -1 * winnings + winnings_text = f"{user_name} lost {money_lost} GwendoBucks" + winnings_text += f" {reason}\n" + final_winnings += winnings_text + else: + if winnings == 1: + final_winnings += f"{user_name} won 1 GwendoBuck {reason}\n" + else: + winnings_text = f"{user_name} won {winnings} GwendoBucks" + winnings_text += f" {reason}\n" + final_winnings += winnings_text + + self.bot.money.addMoney(user, net_winnings) + + self.bot.database["blackjack games"].delete_one({"_id": channel}) + + return final_winnings + + def _calc_winning(self, hand: dict, dealer_value: int, top_level: bool, + dealer_blackjack: bool, dealer_busted: bool): + """ + Calculate how much a user has won/lost in the blackjack game. + + *Parameters* + ------------ + hand: dict + The hand to calculate the winnings of. + dealer_value: int + The dealer's hand value. + top_level: bool + If the input hand is _all_ if the player's hands. If + False, it's one of the hands resulting from a split. + dealer_blackjack: bool + If the dealer has a blackjack. + dealer_busted: bool + If the dealer busted. + + *Returns* + --------- + winnings: int + How much the player has won/lost. + net_winnings: int + winnings minus the original bet. This is added to the + user's account, since the bet was removed from their + account when they placed the bet. + reason: str + The reason for why they won/lost. + """ + self.bot.log("Calculating winnings") + reason = "" + bet = hand["bet"] + winnings = -1 * bet + net_winnings = 0 + hand_value = self._calc_hand_value(hand["hand"]) + + if hand["blackjack"] and not dealer_blackjack: + reason += "(blackjack)" + winnings += math.floor(2.5 * bet) + net_winnings += math.floor(2.5 * bet) + elif dealer_blackjack: + reason += "(dealer blackjack)" + elif hand["busted"]: + reason += "(busted)" + else: + if dealer_busted: + reason = "(dealer busted)" + winnings += 2 * bet + net_winnings += 2 * bet + elif hand_value > dealer_value: + winnings += 2 * bet + net_winnings += 2 * bet + reason = "(highest value)" + elif hand_value == dealer_value: + reason = "(pushed)" + winnings += bet + net_winnings += bet + else: + reason = "(highest value)" + + if top_level: + if hand["split"] >= 1: + calc_winnings_parameters = [ + hand["other hand"], + dealer_value, + False, + dealer_blackjack, + dealer_busted + ] + winnings_calc = self._calc_winning(*calc_winnings_parameters) + winnings_temp, net_winnings_temp, reason_temp = winnings_calc + winnings += winnings_temp + net_winnings += net_winnings_temp + reason += reason_temp + if hand["split"] >= 2: + calc_winnings_parameters = [ + hand["third hand"], + dealer_value, + False, + dealer_blackjack, + dealer_busted + ] + winnings_calc = self._calc_winning(*calc_winnings_parameters) + winnings_temp, net_winnings_temp, reason_temp = winnings_calc + winnings += winnings_temp + net_winnings += net_winnings_temp + reason += reason_temp + if hand["split"] >= 3: + calc_winnings_parameters = [ + hand["fourth hand"], + dealer_value, + False, + dealer_blackjack, + dealer_busted + ] + winnings_calc = self._calc_winning(*calc_winnings_parameters) + winnings_temp, net_winnings_temp, reason_temp = winnings_calc + winnings += winnings_temp + net_winnings += net_winnings_temp + reason += reason_temp + + return winnings, net_winnings, reason + + def _get_hand_number(self, user: dict, hand_number: int): + """ + Get the hand with the given number. + + *Parameters* + ------------ + user: dict + The full hand dict of the user. + hand_number: int + The number of the hand to get. + + *Returns* + --------- + hand: dict + The hand. + hand_number: int + The same as hand_number, except if the user hasn't + split. If the user hasn't split, returns 0. + """ + hand = None + + if user["split"] == 0: + hand = user + hand_number = 0 + else: + if hand_number != 0: + if hand_number == 1: + hand = user + elif hand_number == 2: + hand = user["other hand"] + elif hand_number == 3: + hand = user["third hand"] + elif hand_number == 4: + hand = user["fourth hand"] + + return hand, hand_number + + def _is_round_done(self, game: dict): + """ + Find out if the round is done. + + *Parameters* + ------------ + game: dict + The game to check. + + *Returns* + --------- + round_done: bool + Whether the round is done. + """ + round_done = True + + for person in game["user hands"].values(): + if (not person["hit"]) and (not person["standing"]): + round_done = False + + if person["split"] > 0: + if not person["other hand"]["hit"]: + if not person["other hand"]["standing"]: + round_done = False + + if person["split"] > 1: + if not person["third hand"]["hit"]: + if not person["third hand"]["standing"]: + round_done = False + + if person["split"] > 2: + if not person["fourth hand"]["hit"]: + if not person["fourth hand"]["standing"]: + round_done = False + + return round_done + + async def _blackjack_loop(self, channel, game_round: int, game_id: str): + """ + Run blackjack logic and continue if enough time passes. + + *Parameters* + ------------ + channel: guildChannel or DMChannel + The channel the game is happening in. + game_round: int + The round to start. + game_id: str + The ID of the game. + """ + self.bot.log("Loop "+str(game_round), str(channel.id)) + + old_images_path = "gwendolyn/resources/games/old_images/" + old_image_path = old_images_path + f"blackjack{channel.id}" + with open(old_image_path, "r") as file_pointer: + old_image = await channel.fetch_message(int(file_pointer.read())) + + continue_data = (self._blackjack_continue(str(channel.id))) + new_message, all_standing, game_done = continue_data + if new_message != "": + self.bot.log(new_message, str(channel.id)) + await channel.send(new_message) + + if not game_done: + await old_image.delete() + tables_path = "gwendolyn/resources/games/blackjack_tables/" + file_path = f"{tables_path}blackjack_table{channel.id}.png" + old_image = await channel.send(file=discord.File(file_path)) + with open(old_image_path, "w") as file_pointer: + file_pointer.write(str(old_image.id)) + + if all_standing: + await asyncio.sleep(5) + else: + await asyncio.sleep(120) + + blackjack_games = self.bot.database["blackjack games"] + game = blackjack_games.find_one({"_id": str(channel.id)}) + + if game is None: + right_round = False + else: + real_round = game["round"] or -1 + real_id = game["game_id"] or -1 + right_round = game_round == real_round and game_id == real_id + + if right_round: + if not game_done: + log_message = f"Loop {game_round} starting a new blackjack loop" + self.bot.log(log_message, str(channel.id)) + await self._blackjack_loop(channel, game_round+1, game_id) + else: + new_message = self._blackjack_finish(str(channel.id)) + await channel.send(new_message) + else: + log_message = f"Ending loop on round {game_round}" + self.bot.log(log_message, str(channel.id)) + + async def hit(self, ctx: discord_slash.context.SlashContext, + hand_number: int = 0): + """ + Hit on a hand. + + *Parameters* + ------------ + ctx: discord_slash.context.SlashContext + The context of the command. + hand_number: int = 0 + The number of the hand to hit. + """ + await self.bot.defer(ctx) + channel = str(ctx.channel_id) + user = f"#{ctx.author.id}" + round_done = False + + blackjack_games = self.bot.database["blackjack games"] + game = blackjack_games.find_one({"_id": channel}) + + if user in game["user hands"]: + + user_hands = game["user hands"][user] + hand, hand_number = self._get_hand_number(user_hands, hand_number) + + if hand is None: + log_message = "They didn't specify a hand" + send_message = "You need to specify a hand" + elif game["round"] <= 0: + log_message = "They tried to hit on the 0th round" + send_message = "You can't hit before you see your cards" + elif hand["hit"]: + log_message = "They've already hit this round" + send_message = "You've already hit this round" + elif hand["standing"]: + log_message = "They're already standing" + send_message = "You can't hit when you're standing" + else: + hand["hand"].append(self._draw_card(channel)) + hand["hit"] = True + + hand_value = self._calc_hand_value(hand["hand"]) + + if hand_value > 21: + hand["busted"] = True + + if hand_number == 2: + hand_path = f"user hands.{user}.other hand" + elif hand_number == 3: + hand_path = f"user hands.{user}.third hand" + elif hand_number == 4: + hand_path = f"user hands.{user}.fourth hand" + else: + hand_path = f"user hands.{user}" + + game_updater = {"$set": {hand_path: hand}} + blackjack_games.update_one({"_id": channel}, game_updater) + game = blackjack_games.find_one({"_id": channel}) + round_done = self._is_round_done(game) + + send_message = f"{ctx.author.display_name} hit" + log_message = "They succeeded" + else: + log_message = "They tried to hit without being in the game" + send_message = "You have to enter the game before you can hit" + + await ctx.send(send_message) + self.bot.log(log_message) + + if round_done: + game_id = game["game_id"] + self.bot.log("Hit calling self._blackjack_loop()", channel) + await self._blackjack_loop(ctx.channel, game["round"]+1, game_id) + + async def double(self, ctx: discord_slash.context.SlashContext, + hand_number: int = 0): + """ + Double a hand. + + *Parameters* + ------------ + ctx: discord_slash.context.SlashContext + The context of the command. + hand_number: int = 0 + The number of the hand to double. + """ + await self.bot.defer(ctx) + channel = str(ctx.channel_id) + user = f"#{ctx.author.id}" + round_done = False + blackjack_games = self.bot.database["blackjack games"] + + game = blackjack_games.find_one({"_id": channel}) + + if user in game["user hands"]: + hand_parameters = [game["user hands"][user], hand_number] + hand, hand_number = self._get_hand_number(*hand_parameters) + + if hand is None: + log_message = "They didn't specify a hand" + send_message = "You need to specify a hand" + elif game["round"] <= 0: + log_message = "They tried to hit on the 0th round" + send_message = "You can't hit before you see your cards" + elif hand["hit"]: + log_message = "They've already hit this round" + send_message = "You've already hit this round" + elif hand["standing"]: + log_message = "They're already standing" + send_message = "You can't hit when you're standing" + elif len(hand["hand"]) != 2: + log_message = "They tried to double after round 1" + send_message = "You can only double on the first round" + elif self.bot.money.checkBalance(user) < hand["bet"]: + log_message = "They tried to double without being in the game" + send_message = "You can't double when you're not in the game" + else: + bet = hand["bet"] + self.bot.money.addMoney(user, -1 * bet) + + hand["hand"].append(self._draw_card(channel)) + hand["hit"] = True + hand["doubled"] = True + hand["bet"] += bet + + hand_value = self._calc_hand_value(hand["hand"]) + + if hand_value > 21: + hand["busted"] = True + + if hand_number == 2: + hand_path = f"user hands.{user}.other hand" + elif hand_number == 3: + hand_path = f"user hands.{user}.third hand" + elif hand_number == 4: + hand_path = f"user hands.{user}.fourth hand" + else: + hand_path = f"user hands.{user}" + + game_updater = {"$set": {hand_path: hand}} + blackjack_games.update_one({"_id": channel}, game_updater) + + game = blackjack_games.find_one({"_id": channel}) + round_done = self._is_round_done(game) + + send_message = self.long_strings["Blackjack double"] + user_name = self.bot.database_funcs.get_name(user) + send_message = send_message.format(bet, user_name) + log_message = "They succeeded" + else: + log_message = "They tried to double without being in the game" + send_message = "You can't double when you're not in the game" + + await ctx.send(send_message) + self.bot.log(log_message) + + if round_done: + game_id = game["game_id"] + self.bot.log("Double calling self._blackjack_loop()", channel) + await self._blackjack_loop(ctx.channel, game["round"]+1, game_id) + + async def stand(self, ctx: discord_slash.context.SlashContext, + hand_number: int = 0): + """ + Stand on a hand. + + *Parameters* + ------------ + ctx: discord_slash.context.SlashContext + The context of the command. + hand_number: int = 0 + The number of the hand to stand on. + """ + await self.bot.defer(ctx) + channel = str(ctx.channel_id) + user = f"#{ctx.author.id}" + round_done = False + + blackjack_games = self.bot.database["blackjack games"] + game = blackjack_games.find_one({"_id": channel}) + + if game is not None and user in game["user hands"]: + hand_parameters = [game["user hands"][user], hand_number] + hand, hand_number = self._get_hand_number(*hand_parameters) + + if hand is None: + send_message = "You need to specify which hand" + log_message = "They didn't specify a hand" + elif game["round"] <= 0: + send_message = "You can't stand before you see your cards" + log_message = "They tried to stand on round 0" + elif hand["hit"]: + send_message = "You've already hit this round" + log_message = "They'd already hit this round" + elif hand["standing"]: + send_message = "You're already standing" + log_message = "They're already standing" + else: + hand["standing"] = True + + if hand_number == 2: + hand_path = f"user hands.{user}.other hand" + elif hand_number == 3: + hand_path = f"user hands.{user}.third hand" + elif hand_number == 4: + hand_path = f"user hands.{user}.fourth hand" + else: + hand_path = f"user hands.{user}" + + game_updater = {"$set": {hand_path: hand}} + blackjack_games.update_one({"_id": channel}, game_updater) + game = blackjack_games.find_one({"_id": channel}) + round_done = self._is_round_done(game) + + send_message = f"{ctx.author.display_name} is standing" + log_message = "They succeeded" + + else: + log_message = "They tried to stand without being in the game" + send_message = "You have to enter the game before you can stand" + + await ctx.send(send_message) + self.bot.log(log_message) + + if round_done: + game_id = game["game_id"] + self.bot.log("Stand calling self._blackjack_loop()", channel) + await self._blackjack_loop(ctx.channel, game["round"]+1, game_id) + + async def split(self, ctx: discord_slash.context.SlashContext, + hand_number: int = 0): + """ + Split a hand. + + *Parameters* + ------------ + ctx: discord_slash.context.SlashContext + The context of the command. + hand_number: int = 0 + The number of the hand to split. + """ + await self.bot.defer(ctx) + channel = str(ctx.channel_id) + user = f"#{ctx.author.id}" + round_done = False + hand_number_error = False + + blackjack_games = self.bot.database["blackjack games"] + game = blackjack_games.find_one({"_id": channel}) + + if game["user hands"][user]["split"] == 0: + hand = game["user hands"][user] + new_hand = game["user hands"][user]["other hand"] + hand_number = 0 + other_hand = 2 + else: + if hand_number == 1: + hand = game["user hands"][user] + elif hand_number == 2: + hand = game["user hands"][user]["other hand"] + elif hand_number == 3: + hand = game["user hands"][user]["third hand"] + else: + hand_number_error = True + + if game["user hands"][user]["split"] == 1: + new_hand = game["user hands"][user]["third hand"] + other_hand = 3 + else: + new_hand = game["user hands"][user]["fourth hand"] + other_hand = 4 + + if hand_number_error: + log_message = "They didn't specify a hand" + send_message = "You have to specify the hand you're hitting with" + elif game["round"] == 0: + log_message = "They tried to split on round 0" + send_message = "You can't split before you see your cards" + elif game["user hands"][user]["split"] > 3: + log_message = "They tried to split more than three times" + send_message = "You can only split 3 times" + elif hand["hit"]: + log_message = "They've already hit" + send_message = "You've already hit or split this hand." + elif hand["standing"]: + log_message = "They're already standing" + send_message = "You're already standing" + elif len(hand["hand"]) != 2: + log_message = "They tried to split after the first round" + send_message = "You can only split on the first round" + else: + first_card = self._calc_hand_value([hand["hand"][0]]) + second_card = self._calc_hand_value([hand["hand"][1]]) + if first_card != second_card: + log_message = "They tried to split two different cards" + send_message = self.long_strings["Blackjack different cards"] + else: + bet = hand["bet"] + if self.bot.money.checkBalance(user) < bet: + log_message = "They didn't have enough GwendoBucks" + send_message = "You don't have enough GwendoBucks" + else: + self.bot.money.addMoney(user, -1 * bet) + + hand["hit"] = True + new_hand["hit"] = True + + new_hand = { + "hand": [], "bet": 0, "standing": False, + "busted": False, "blackjack": False, "hit": True, + "doubled": False + } + + new_hand["bet"] = hand["bet"] + + new_hand["hand"].append(hand["hand"].pop(1)) + new_hand["hand"].append(self._draw_card(channel)) + hand["hand"].append(self._draw_card(channel)) + hand["hit"] = True + + hand_value = self._calc_hand_value(hand["hand"]) + other_hand_value = self._calc_hand_value(new_hand["hand"]) + if hand_value > 21: + hand["busted"] = True + elif hand_value == 21: + hand["blackjack"] = True + + if other_hand_value > 21: + new_hand["busted"] = True + elif other_hand_value == 21: + new_hand["blackjack"] = True + + if hand_number == 2: + hand_path = f"user hands.{user}.other hand" + elif hand_number == 3: + hand_path = f"user hands.{user}.third hand" + else: + hand_path = f"user hands.{user}" + + game_updater = {"$set": {hand_path: hand}} + blackjack_games.update_one({"_id": channel}, game_updater) + + if other_hand == 3: + other_hand_path = f"user hands.{user}.third hand" + elif other_hand == 4: + other_hand_path = f"user hands.{user}.fourth hand" + else: + other_hand_path = f"user hands.{user}.other hand" + + game_updater = {"$set": {other_hand_path: new_hand}} + blackjack_games.update_one({"_id": channel}, game_updater) + + split_updater = {"$inc": {"user hands."+user+".split": 1}} + blackjack_games.update_one({"_id": channel}, split_updater) + + game = blackjack_games.find_one({"_id": channel}) + round_done = self._is_round_done(game) + + send_message = self.long_strings["Blackjack split"] + user_name = self.bot.database_funcs.get_name(user) + send_message = send_message.format(user_name) + log_message = "They succeeded" + + await ctx.send(send_message) + self.bot.log(log_message) + + if round_done: + game_id = game["game_id"] + self.bot.log("Stand calling self._blackjack_loop()", channel) + await self._blackjack_loop(ctx.channel, game["round"]+1, game_id) + + async def enter_game(self, ctx: discord_slash.context.SlashContext, + bet: int): + """ + Enter the blackjack game. + + *Parameters* + ------------ + ctx: discord_slash.context.SlashContext + The context of the command. + bet: int + The bet to enter with. + """ + await self.bot.defer(ctx) + channel = str(ctx.channel_id) + user = f"#{ctx.author.id}" + collection = self.bot.database["blackjack games"] + game = collection.find_one({"_id": channel}) + user_name = self.bot.database_funcs.get_name(user) + + self.bot.log(f"{user_name} is trying to join the Blackjack game") + + if game is None: + send_message = "There is no game going on in this channel" + log_message = send_message + elif user in game["user hands"]: + send_message = "You're already in the game!" + log_message = "They're already in the game" + elif len(game["user hands"]) >= 5: + send_message = "There can't be more than 5 players in a game" + log_message = "There were already 5 players in the game" + elif game["round"] != 0: + send_message = "The table is no longer taking bets" + log_message = "They tried to join after the game begun" + elif bet < 0: + send_message = "You can't bet a negative amount" + log_message = "They tried to bet a negative amount" + elif self.bot.money.checkBalance(user) < bet: + send_message = "You don't have enough GwendoBucks" + log_message = "They didn't have enough GwendoBucks" + else: + self.bot.money.addMoney(user, -1 * bet) + player_hand = [self._draw_card(channel) for _ in range(2)] + + hand_value = self._calc_hand_value(player_hand) + + blackjack_hand = (hand_value == 21) + + new_hand = { + "hand": player_hand, "bet": bet, "standing": False, + "busted": False, "blackjack": blackjack_hand, + "hit": True, "doubled": False, "split": 0, + "other hand": {}, "third hand": {}, "fourth hand": {} + } + + function = {"$set": {f"user hands.{user}": new_hand}} + collection.update_one({"_id": channel}, function) + + enter_game_text = "entered the game with a bet of" + bet_text = f"{bet} GwendoBucks" + send_message = f"{user_name} {enter_game_text} {bet_text}" + log_message = send_message + + self.bot.log(log_message) + await ctx.send(send_message) + + async def start(self, ctx: discord_slash.context.SlashContext): + """ + Start a blackjack game. + + *Parameters* + ------------ + ctx: discord_slash.context.SlashContext + The context of the command. + """ + await self.bot.defer(ctx) + channel = str(ctx.channel_id) + blackjack_min_cards = 50 + + self.bot.log("Starting blackjack game") + await ctx.send("Starting a new game of blackjack") + cards_left = 0 + cards = self.bot.database["blackjack cards"].find_one({"_id": channel}) + if cards is not None: + cards_left = len(cards["cards"]) + + # Shuffles if not enough cards + if cards_left < blackjack_min_cards: + self._blackjack_shuffle(channel) + self.bot.log("Shuffling the blackjack deck...", channel) + await ctx.channel.send("Shuffling the deck...") + + game = self.bot.database["blackjack games"].find_one({"_id": channel}) + + self.bot.log("Trying to start a blackjack game in "+channel) + game_started = False + + if game is None: + + dealer_hand = [self._draw_card(channel), self._draw_card(channel)] + game_id = datetime.datetime.now().strftime('%Y%m%d%H%M%S') + + new_game = { + "_id": channel, "dealer hand": dealer_hand, + "dealer busted": False, "dealer blackjack": False, + "user hands": {}, "all standing": False, "round": 0, + "game_id": game_id + } + + if self._calc_hand_value(dealer_hand) == 21: + new_game["dealer blackjack"] = True + + self.bot.database["blackjack games"].insert_one(new_game) + + table_images_path = "gwendolyn/resources/games/blackjack_tables/" + empty_table_image = "gwendolyn/resources/games/blackjack_table.png" + new_table_image = f"{table_images_path}blackjack_table{channel}.png" + copyfile(empty_table_image, new_table_image) + + game_started = True + + if game_started: + send_message = self.long_strings["Blackjack started"] + await ctx.channel.send(send_message) + + table_images_path = "gwendolyn/resources/games/blackjack_tables/" + file_path = f"{table_images_path}blackjack_table{channel}.png" + + old_image = await ctx.channel.send(file=discord.File(file_path)) + old_images_path = "gwendolyn/resources/games/old_images/blackjack" + + with open(old_images_path+channel, "w") as file_pointer: + file_pointer.write(str(old_image.id)) + + await asyncio.sleep(30) + + game_done = False + + blackjack_games = self.bot.database["blackjack games"] + game = blackjack_games.find_one({"_id": channel}) + + if len(game["user hands"]) == 0: + game_done = True + send_message = "No one entered the game. Ending the game." + await ctx.channel.send(send_message) + + game_id = game["game_id"] + + # Loop of game rounds + if not game_done: + self.bot.log("start() calling _blackjack_loop()", channel) + await self._blackjack_loop(ctx.channel, 1, game_id) + else: + new_message = self._blackjack_finish(channel) + await ctx.channel.send(new_message) + else: + send_message = self.long_strings["Blackjack going on"] + await ctx.channel.send(send_message) + self.bot.log("There was already a game going on") + + async def hilo(self, ctx: discord_slash.context.SlashContext): + """ + Get the hilo of the blackjack game. + + *Parameters* + ------------ + ctx: discord_slash.context.SlashContext + The context of the command. + """ + channel = ctx.channel_id + data = self.bot.database["hilo"].find_one({"_id": str(channel)}) + if data is not None: + hilo = str(data["hilo"]) + else: + hilo = "0" + await ctx.send(f"Hi-lo value: {hilo}", hidden=True) + + async def shuffle(self, ctx: discord_slash.context.SlashContext): + """ + Shuffle the cards used for blackjack. + + *Parameters* + ------------ + ctx: discord_slash.context.SlashContext + The context of the command. + """ + channel = ctx.channel_id + self._blackjack_shuffle(str(channel)) + self.bot.log("Shuffling the blackjack deck...", str(channel)) + await ctx.send("Shuffling the deck...") + + async def cards(self, ctx: discord_slash.context.SlashContext): + """ + Get how many cards are left for blackjack. + + *Parameters* + ------------ + ctx: discord_slash.context.SlashContext + The context of the command. + """ + channel = ctx.channel_id + cards_left = 0 + blackjack_games = self.bot.database["blackjack cards"] + cards = blackjack_games.find_one({"_id": str(channel)}) + if cards is not None: + cards_left = len(cards["cards"]) + + decks_left = round(cards_left/52, 1) + send_message = f"Cards left:\n{cards_left} cards, {decks_left} decks" + await ctx.send(send_message, hidden=True) + + +class DrawBlackjack(): + """ + Draws the blackjack image. + + *Methods* + --------- + draw_image(channel: str) + + *Attributes* + ------------ + bot: Gwendolyn + The instance of the bot. + BORDER: int + The size of the border in pixels. + PLACEMENT: list + The order to place the user hands in. + """ + # pylint: disable=too-few-public-methods + + def __init__(self, bot): + """Initialize the class.""" + self.bot = bot + # pylint: disable=invalid-name + self.BORDER = 100 + self.SMALLBORDER = int(self.BORDER/3.5) + self.PLACEMENT = [2, 1, 3, 0, 4] + # pylint: enable=invalid-name + + def draw_image(self, channel: str): + """ + Draw the table image. + + *Parameters* + ------------ + channel: str + The id of the channel the game is in. + """ + self.bot.log("Drawing blackjack table", channel) + game = self.bot.database["blackjack games"].find_one({"_id": channel}) + + fonts_path = "gwendolyn/resources/fonts/" + font = ImageFont.truetype(fonts_path+'futura-bold.ttf', 50) + small_font = ImageFont.truetype(fonts_path+'futura-bold.ttf', 40) + + table = Image.open("gwendolyn/resources/games/blackjack_table.png") + text_image = ImageDraw.Draw(table) + hands = game["user hands"] + + dealer_busted = game["dealer busted"] + dealer_blackjack = game["dealer blackjack"] + + if not game["all standing"]: + hand_parameters = [ + game["dealer hand"], + True, + False, + False + ] + else: + hand_parameters = [ + game["dealer hand"], + False, + dealer_busted, + dealer_blackjack + ] + + dealer_hand = self._draw_hand(*hand_parameters) + + paste_position = (800-self.SMALLBORDER, 20-self.SMALLBORDER) + table.paste(dealer_hand, paste_position, dealer_hand) + + for i in range(len(hands)): + key, value = list(hands.items())[i] + key = self.bot.database_funcs.get_name(key) + hand_parameters = [ + value["hand"], + False, + value["busted"], + value["blackjack"] + ] + user_hand = self._draw_hand(*hand_parameters) + position_x = 32-self.SMALLBORDER+(384*self.PLACEMENT[i]) + + if value["split"] >= 1: + hand_parameters_two = [ + value["other hand"]["hand"], + False, + value["other hand"]["busted"], + value["other hand"]["blackjack"] + ] + user_other_hand = self._draw_hand(*hand_parameters_two) + + if value["split"] >= 2: + hand_parameters_three = [ + value["third hand"]["hand"], + False, + value["third hand"]["busted"], + value["third hand"]["blackjack"] + ] + user_third_hand = self._draw_hand(*hand_parameters_three) + + if value["split"] >= 3: + hand_parameters_four = [ + value["fourth hand"]["hand"], + False, + value["fourth hand"]["busted"], + value["fourth hand"]["blackjack"] + ] + user_fourth_hand = self._draw_hand(*hand_parameters_four) + + if value["split"] == 3: + position_one = (position_x, 280-self.SMALLBORDER) + position_two = (position_x, 420-self.SMALLBORDER) + position_three = (position_x, 560-self.SMALLBORDER) + position_four = (position_x, 700-self.SMALLBORDER) + + table.paste(user_hand, position_one, user_hand) + table.paste(user_other_hand, position_two, user_other_hand) + table.paste(user_third_hand, position_three, user_third_hand) + table.paste(user_fourth_hand, position_four, user_fourth_hand) + elif value["split"] == 2: + position_one = (position_x, 420-self.SMALLBORDER) + position_two = (position_x, 560-self.SMALLBORDER) + position_three = (position_x, 700-self.SMALLBORDER) + + table.paste(user_hand, position_one, user_hand) + table.paste(user_other_hand, position_two, user_other_hand) + table.paste(user_third_hand, position_three, user_third_hand) + elif value["split"] == 1: + position_one = (position_x, 560-self.SMALLBORDER) + position_two = (position_x, 700-self.SMALLBORDER) + + table.paste(user_hand, position_one, user_hand) + table.paste(user_other_hand, position_two, user_other_hand) + else: + position_one = (position_x, 680-self.SMALLBORDER) + table.paste(user_hand, position_one, user_hand) + + text_width = font.getsize(key)[0] + text_x = 32+(384*self.PLACEMENT[i])+117-int(text_width/2) + black = (0, 0, 0) + white = (255, 255, 255) + + if text_width < 360: + # Black shadow behind and slightly below white text + text_image.text((text_x-3, 1010-3), key, fill=black, font=font) + text_image.text((text_x+3, 1010-3), key, fill=black, font=font) + text_image.text((text_x-3, 1010+3), key, fill=black, font=font) + text_image.text((text_x+3, 1010+3), key, fill=black, font=font) + text_image.text((text_x, 1005), key, fill=white, font=font) + else: + text_width = small_font.getsize(key)[0] + positions = [ + (text_x-2, 1020-2), + (text_x+2, 1020-2), + (text_x-2, 1020+2), + (text_x+2, 1020+2), + (text_x, 1015) + ] + text_image.text(positions[0], key, fill=black, font=small_font) + text_image.text(positions[1], key, fill=black, font=small_font) + text_image.text(positions[2], key, fill=black, font=small_font) + text_image.text(positions[3], key, fill=black, font=small_font) + text_image.text(positions[4], key, fill=white, font=small_font) + + self.bot.log("Saving table image") + table_images_path = "gwendolyn/resources/games/blackjack_tables/" + table_image_path = f"{table_images_path}blackjack_table{channel}.png" + table.save(table_image_path) + + return + + def _draw_hand(self, hand: dict, dealer: bool, busted: bool, + blackjack: bool): + """ + Draw a hand. + + *Parameters* + ------------ + hand: dict + The hand to draw. + dealer: bool + If the hand belongs to the dealer who hasn't shown + their second card. + busted: bool + If the hand is busted. + blackjack: bool + If the hand has a blackjack. + + *Returns* + --------- + background: Image + The image of the hand. + """ + self.bot.log("Drawing hand {hand}, {busted}, {blackjack}") + fonts_path = "gwendolyn/resources/fonts/" + font = ImageFont.truetype(fonts_path+'futura-bold.ttf', 200) + font_two = ImageFont.truetype(fonts_path+'futura-bold.ttf', 120) + length = len(hand) + background_width = (self.BORDER*2)+691+(125*(length-1)) + background_size = (background_width, (self.BORDER*2)+1065) + background = Image.new("RGBA", background_size, (0, 0, 0, 0)) + text_image = ImageDraw.Draw(background) + card_y = self.BORDER+self.PLACEMENT[1] + cards_path = "gwendolyn/resources/games/cards/" + + if dealer: + img = Image.open(cards_path+hand[0].upper()+".png") + card_position = (self.BORDER+self.PLACEMENT[0], card_y) + background.paste(img, card_position, img) + img = Image.open("gwendolyn/resources/games/cards/red_back.png") + card_position = (125+self.BORDER+self.PLACEMENT[0], card_y) + background.paste(img, card_position, img) + else: + for i in range(length): + card_path = cards_path + f"{hand[i].upper()}.png" + img = Image.open(card_path) + card_position = (self.BORDER+(i*125)+self.PLACEMENT[0], card_y) + background.paste(img, card_position, img) + + width, height = background.size + text_height = 290+self.BORDER + white = (255, 255, 255) + black = (0, 0, 0) + red = (255, 50, 50) + gold = (155, 123, 0) + + if busted: + text_width = font.getsize("BUSTED")[0] + text_x = int(width/2)-int(text_width/2) + positions = [ + (text_x-10, text_height+20-10), + (text_x+10, text_height+20-10), + (text_x-10, text_height+20+10), + (text_x+10, text_height+20+10), + (text_x-5, text_height-5), + (text_x+5, text_height-5), + (text_x-5, text_height+5), + (text_x+5, text_height+5), + (text_x, text_height) + ] + text_image.text(positions[0], "BUSTED", fill=black, font=font) + text_image.text(positions[1], "BUSTED", fill=black, font=font) + text_image.text(positions[2], "BUSTED", fill=black, font=font) + text_image.text(positions[3], "BUSTED", fill=black, font=font) + text_image.text(positions[4], "BUSTED", fill=white, font=font) + text_image.text(positions[5], "BUSTED", fill=white, font=font) + text_image.text(positions[6], "BUSTED", fill=white, font=font) + text_image.text(positions[7], "BUSTED", fill=white, font=font) + text_image.text(positions[8], "BUSTED", fill=red, font=font) + elif blackjack: + text_width = font_two.getsize("BLACKJACK")[0] + text_x = int(width/2)-int(text_width/2) + positions = [ + (text_x-6, text_height+20-6), + (text_x+6, text_height+20-6), + (text_x-6, text_height+20+6), + (text_x+6, text_height+20+6), + (text_x-3, text_height-3), + (text_x+3, text_height-3), + (text_x-3, text_height+3), + (text_x+3, text_height+3), + (text_x, text_height) + ] + text = "BLACKJACK" + text_image.text(positions[0], text, fill=black, font=font_two) + text_image.text(positions[1], text, fill=black, font=font_two) + text_image.text(positions[2], text, fill=black, font=font_two) + text_image.text(positions[3], text, fill=black, font=font_two) + text_image.text(positions[4], text, fill=white, font=font_two) + text_image.text(positions[5], text, fill=white, font=font_two) + text_image.text(positions[6], text, fill=white, font=font_two) + text_image.text(positions[7], text, fill=white, font=font_two) + text_image.text(positions[8], text, fill=gold, font=font_two) + + resized_size = (int(width/3.5), int(height/3.5)) + return background.resize(resized_size, resample=Image.BILINEAR) diff --git a/gwendolyn/funcs/games/connect_four.py b/gwendolyn/funcs/games/connect_four.py new file mode 100644 index 0000000..7dc0322 --- /dev/null +++ b/gwendolyn/funcs/games/connect_four.py @@ -0,0 +1,1099 @@ +""" +Contains the classes that deal with playing and drawing connect four. + +*Classes* +--------- + ConnectFour + DrawConnectFour +""" +import random # Used to shuffle players and by the ai to pick from +# similar options +import copy # Used to deepcopy boards +from typing import Union # Used for typehints +import math # Used for math.inf +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 + +ROWCOUNT = 6 +COLUMNCOUNT = 7 + + +class ConnectFour(): + """ + Deals with connect four commands and logic. + + *Methods* + --------- + start(ctx: SlashContext, opponent: Union[int, Discord.User]) + place_piece(ctx: Union[SlashContext, discord.Message], + user: int, column: int) + surrender(ctx: SlashContext) + """ + + def __init__(self, bot): + """Initialize the class.""" + self.bot = bot + self.draw = DrawConnectFour(bot) + self.database_funcs = self.bot.database_funcs + # pylint: disable=invalid-name + self.AISCORES = { + "middle": 3, + "two in a row": 10, + "three in a row": 50, + "enemy two in a row": -35, + "enemy three in a row": -200, + "enemy win": -10000, + "win": 10000, + "avoid losing": 100 + } + self.REACTIONS = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣"] + # pylint: enable=invalid-name + + async def start(self, ctx: SlashContext, + opponent: Union[int, discord.User]): + """ + Start a game of connect four. + + *Parameters* + ------------ + ctx: SlashContext + The context of the slash command. + opponent: Union[int, discord.User] + The requested opponent. If it's an int, the opponent is + Gwendolyn with a difficulty equal to the value of the + int. The difficulty is equal to how many levels the AI + searches when minimaxing. + """ + await self.bot.defer(ctx) + user = f"#{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): + # Opponent is Gwendolyn + if opponent in range(1, 6): + difficulty = int(opponent) + difficulty_text = f" with difficulty {difficulty}" + opponent = f"#{self.bot.user.id}" + else: + send_message = "Difficulty doesn't exist" + log_message = "They challenged a difficulty that doesn't exist" + can_start = False + elif isinstance(opponent, discord.User): + if opponent.bot: + # User has challenged a bot + if opponent == self.bot.user: + # It was Gwendolyn + difficulty = 3 + difficulty_text = f" with difficulty {difficulty}" + opponent = f"#{self.bot.user.id}" + else: + send_message = "You can't challenge a bot!" + log_message = "They tried to challenge a bot" + can_start = False + else: + # Opponent is another player + if ctx.author != opponent: + opponent = f"#{opponent.id}" + difficulty = 5 + difficulty_text = "" + else: + send_message = "You can't play against yourself" + log_message = "They tried to play against themself" + can_start = False + + if can_start: + board = [[0 for _ in range(COLUMNCOUNT)] for _ in range(ROWCOUNT)] + 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.bot.database["connect 4 games"].insert_one(new_game) + + self.draw.draw_image(channel) + + gwendolyn_turn = (players[0] == f"#{self.bot.user.id}") + started_game = True + + opponent_name = self.database_funcs.get_name(opponent) + turn_name = self.database_funcs.get_name(players[0]) + + started_text = "Started game against {}{}.".format( + opponent_name, + difficulty_text + ) + turn_text = f"It's {turn_name}'s turn" + send_message = f"{started_text} {turn_text}" + 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)) + + if gwendolyn_turn: + await self._connect_four_ai(ctx) + else: + for reaction in self.REACTIONS: + await old_image.add_reaction(reaction) + + async def place_piece(self, ctx: Union[SlashContext, discord.Message], + user: int, column: 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) + 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" + else: + board = game["board"] + 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: + 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) + + self.bot.log("Checking for win") + won, win_direction, win_coordinates = self._is_won(board) + + 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) + + 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) + + 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)) + + if game_won: + self._end_game(channel) + else: + with open(old_image_path, "w") as file_pointer: + file_pointer.write(str(old_image.id)) + if gwendolyn_turn: + await self._connect_four_ai(ctx) + else: + for reaction in self.REACTIONS: + await old_image.add_reaction(reaction) + + async def surrender(self, ctx: SlashContext): + """ + Surrender a connect four game. + + *Parameters* + ------------ + 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) + + 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" + + 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) + + if old_image is not None: + await old_image.delete() + else: + self.bot.log("The old image was already deleted") + + 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): + """ + Place a piece in a given board. + + This differs from place_piece() by not having anything to do + with any actual game. It just places the piece in a game dict. + + *Parameters* + ------------ + board: dict + The board to place the piece in. + player: int + The player who places the piece. + column: int + The column the piece is placed in. + + *Returns* + --------- + board: dict + The board with the placed piece. + """ + placement_x, placement_y = -1, column + + for i, line in list(enumerate(board))[::-1]: + if line[column] == 0: + placement_x = i + break + + board[placement_x][placement_y] = player + + if placement_x == -1: + return None + else: + return board + + def _end_game(self, channel: str): + game = self.bot.database["connect 4 games"].find_one({"_id": channel}) + + winner = game["winner"] + if winner != 0: + if game["players"][winner-1] != f"#{self.bot.user.id}": + difficulty = int(game["difficulty"]) + reward = difficulty**2 + 5 + self.bot.money.addMoney(game["players"][winner-1], reward) + + self.database_funcs.delete_game("connect 4 games", channel) + + def _is_won(self, board: dict): + won = 0 + win_direction = "" + win_coordinates = [0, 0] + + for row in range(ROWCOUNT): + for place in range(COLUMNCOUNT): + if won == 0: + piece_player = board[row][place] + if piece_player != 0: + # Checks horizontal + if place <= COLUMNCOUNT-4: + piece_one = board[row][place+1] + piece_two = board[row][place+2] + piece_three = board[row][place+3] + + pieces = [piece_one, piece_two, piece_three] + else: + pieces = [0] + + if all(x == piece_player for x in pieces): + won = piece_player + win_direction = "h" + win_coordinates = [row, place] + + # Checks vertical + if row <= ROWCOUNT-4: + piece_one = board[row+1][place] + piece_two = board[row+2][place] + piece_three = board[row+3][place] + + pieces = [piece_one, piece_two, piece_three] + else: + pieces = [0] + + if all(x == piece_player for x in pieces): + won = piece_player + win_direction = "v" + win_coordinates = [row, place] + + # Checks right diagonal + good_row = (row <= ROWCOUNT-4) + good_column = (place <= COLUMNCOUNT-4) + if good_row and good_column: + piece_one = board[row+1][place+1] + piece_two = board[row+2][place+2] + piece_three = board[row+3][place+3] + + pieces = [piece_one, piece_two, piece_three] + else: + pieces = [0] + + if all(x == piece_player for x in pieces): + won = piece_player + win_direction = "r" + win_coordinates = [row, place] + + # Checks left diagonal + if row <= ROWCOUNT-4 and place >= 3: + piece_one = board[row+1][place-1] + piece_two = board[row+2][place-2] + piece_three = board[row+3][place-3] + + pieces = [piece_one, piece_two, piece_three] + else: + pieces = [0] + + if all(i == piece_player for i in pieces): + won = piece_player + win_direction = "l" + win_coordinates = [row, place] + + return won, win_direction, win_coordinates + + async def _connect_four_ai(self, ctx: SlashContext): + 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"] + + scores = [-math.inf for _ in range(COLUMNCOUNT)] + for column in range(COLUMNCOUNT): + test_board = copy.deepcopy(board) + test_board = self._place_on_board(test_board, player, column) + if test_board is not None: + minimax_parameters = [ + test_board, + difficulty, + player % 2+1, + player, + -math.inf, + math.inf, + False + ] + scores[column] = await self._minimax(*minimax_parameters) + self.bot.log(f"Best score for column {column} is {scores[column]}") + + possible_scores = scores.copy() + + while out_of_range(possible_scores): + possible_scores.remove(min(possible_scores)) + + highest_score = random.choice(possible_scores) + + 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) + + def _ai_calc_points(self, board: dict, player: int): + score = 0 + other_player = player % 2+1 + + # Adds points for middle placement + # Checks horizontal + for row in range(ROWCOUNT): + if board[row][3] == player: + score += self.AISCORES["middle"] + row_array = [int(i) for i in list(board[row])] + for place in range(COLUMNCOUNT-3): + window = row_array[place:place+4] + score += self._evaluate_window(window, player, other_player) + + # Checks Vertical + for column in range(COLUMNCOUNT): + column_array = [int(i[column]) for i in list(board)] + for place in range(ROWCOUNT-3): + window = column_array[place:place+4] + score += self._evaluate_window(window, player, other_player) + + # Checks right diagonal + for row in range(ROWCOUNT-3): + for place in range(COLUMNCOUNT-3): + piece_one = board[row][place] + piece_two = board[row+1][place+1] + piece_three = board[row+2][place+2] + piece_four = board[row+3][place+3] + + window = [piece_one, piece_two, piece_three, piece_four] + score += self._evaluate_window(window, player, other_player) + + for place in range(3, COLUMNCOUNT): + piece_one = board[row][place] + piece_two = board[row+1][place-1] + piece_three = board[row+2][place-2] + piece_four = board[row+3][place-3] + + window = [piece_one, piece_two, piece_three, piece_four] + score += self._evaluate_window(window, player, other_player) + + return score + + def _evaluate_window(self, window: list, player: int, other_player: int): + if window.count(player) == 4: + return self.AISCORES["win"] + elif window.count(player) == 3 and window.count(0) == 1: + return self.AISCORES["three in a row"] + elif window.count(player) == 2 and window.count(0) == 2: + return self.AISCORES["two in a row"] + elif window.count(other_player) == 4: + return self.AISCORES["enemy win"] + else: + return 0 + + async def _minimax(self, board: dict, depth: int, player: int, + original_player: int, alpha: int, beta: int, + maximizing_player: bool): + """ + Evaluate the minimax value of a board. + + *Parameters* + ------------ + board: dict + The board to evaluate. + depth: int + How many more levels to check. If 0, the current value + is returned. + player: int + The player who can place a piece. + original_player: int + The AI player. + alpha, beta: int + Used with alpha/beta pruning, in order to prune options + that will never be picked. + maximizing_player: bool + Whether the current player is the one trying to + maximize their score. + + *Returns* + --------- + value: int + The minimax value of the board. + """ + terminal = 0 not in board[0] + points = self._ai_calc_points(board, original_player) + # The depth is how many moves ahead the computer checks. This + # value is the difficulty. + if depth == 0 or terminal or (points > 5000 or points < -6000): + return points + if maximizing_player: + value = -math.inf + for column in range(0, COLUMNCOUNT): + test_board = copy.deepcopy(board) + test_board = self._place_on_board(test_board, player, column) + if test_board is not None: + minimax_parameters = [ + test_board, + depth-1, + (player % 2)+1, + original_player, + alpha, + beta, + False + ] + evaluation = await self._minimax(*minimax_parameters) + if evaluation < -9000: + evaluation += self.AISCORES["avoid losing"] + value = max(value, evaluation) + alpha = max(alpha, evaluation) + if beta <= alpha: + break + return value + else: + value = math.inf + for column in range(0, COLUMNCOUNT): + test_board = copy.deepcopy(board) + test_board = self._place_on_board(test_board, player, column) + if test_board is not None: + minimax_parameters = [ + test_board, + depth-1, + (player % 2)+1, + original_player, + alpha, + beta, + True + ] + evaluation = await self._minimax(*minimax_parameters) + if evaluation < -9000: + evaluation += self.AISCORES["avoid losing"] + value = min(value, evaluation) + beta = min(beta, evaluation) + if beta <= alpha: + break + return value + + +class DrawConnectFour(): + """ + Functions for drawing the connect four board. + + *Methods* + --------- + draw_image(channel: str) + + *Attributes* + ------------ + BORDER: int + The border around the game board. + GRIDBORDER: int + The border between the edge of the board and the piece + furthest towards the edge. + CORNERSIZE: int + The size of the corner circles. + BOARDOUTLINESIZE: int + How big the outline of the board is. + BOARDOUTLINECOLOR: RGBA tuple + The color of the outline of the board. + PIECEOUTLINESIZE: int + How big the outline of each piece is. + PIECEOUTLINECOLOR: RGB tuple + The color of the outline of each piece. + EMPTYOUTLINESIZE: int + How big the outline of an empty place on the board is. + EMPTYOUTLINECOLOR: RGB tuple + The color of the outline of an empty place on the board. + BOTTOMBORDER: int + The border at the bottom where the names are. + TEXTSIZE: int + The size of the text. + TEXTPADDING: int + How much padding to add to the the text. + WIDTH, HEIGHT: int + The size of the image, minus the bottom border + BACKGROUNDCOLOR: RGBA tuple + The color of the background. + PLAYER1COLOR, PLAYER2COLOR: RGB tuple + The color of each player. + BOARDCOLOR: RGB tuple + The color of the board + WINBARCOLOR: RGBA tuple + The color of the bar placed over the winning pieces. + PIECESIZE: int + The size of each piece. + FONT: FreeTypeFont + The font to use to write text. + PIECEGRIDSIZE: list + The size of each grid cell on the board. + PIECESTARTX, PIECESTARTY: int + Where the top left piece should be placed. + WINBARWHITE: RGBA tuple + White, but with the alpha set to win_bar_alpha. + """ + + def __init__(self, bot): + """Initialize the class.""" + self.bot = bot + self.database_funcs = self.bot.database_funcs + win_bar_alpha = 160 + font_path = "gwendolyn/resources/fonts/futura-bold.ttf" + + # pylint: disable=invalid-name + self.BORDER = 40 + self.GRIDBORDER = 40 + self.CORNERSIZE = 300 + self.BOARDOUTLINESIZE = 10 + self.BOARDOUTLINECOLOR = (0, 0, 0) + + self.PIECEOUTLINESIZE = 10 + self.PIECEOUTLINECOLOR = (244, 244, 248) + self.EMPTYOUTLINESIZE = 0 + self.EMPTYOUTLINECOLOR = (0, 0, 0) + + self.BOTTOMBORDER = 110 + self.TEXTSIZE = 100 + self.TEXTPADDING = 20 + self.WIDTH, self.HEIGHT = 2800, 2400 + self.BACKGROUNDCOLOR = (230, 230, 234, 255) + self.PLAYER1COLOR = (254, 74, 73) + self.PLAYER2COLOR = (254, 215, 102) + self.BOARDCOLOR = (42, 183, 202) + self.WINBARCOLOR = (250, 250, 250, 255) + self.PIECESIZE = 300 + # pylint: enable=invalid-name + + board_width = self.WIDTH-(2*(self.BORDER+self.GRIDBORDER)) + board_height = self.HEIGHT-(2*(self.BORDER+self.GRIDBORDER)) + board_size = [board_width, board_height] + piece_width = board_size[0]//COLUMNCOUNT + piece_height = board_size[1]//ROWCOUNT + piece_start = (self.BORDER+self.GRIDBORDER)-(self.PIECESIZE//2) + + # pylint: disable=invalid-name + self.FONT = ImageFont.truetype(font_path, self.TEXTSIZE) + self.PIECEGRIDSIZE = [piece_width, piece_height] + + self.PIECESTARTX = piece_start+(self.PIECEGRIDSIZE[0]//2) + self.PIECESTARTY = piece_start+(self.PIECEGRIDSIZE[1]//2) + + self.WINBARWHITE = (255, 255, 255, win_bar_alpha) + # pylint: enable=invalid-name + + # Draws the whole thing + def draw_image(self, channel: str): + """ + 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) + drawer = ImageDraw.Draw(background, "RGBA") + + self._draw_board(drawer) + + self._draw_pieces(drawer, board) + + if game["winner"] != 0: + self._draw_win(background, game) + + self._draw_footer(drawer, game) + + boards_path = "gwendolyn/resources/games/connect_four_boards/" + background.save(boards_path + f"board{channel}.png") + + def _draw_board(self, drawer: ImageDraw): + # This whole part was the easiest way to make a rectangle with + # rounded corners and an outline + draw_parameters = { + "fill": self.BOARDCOLOR, + "outline": self.BOARDOUTLINECOLOR, + "width": self.BOARDOUTLINESIZE + } + + # - Corners -: + corner_positions = [ + [ + ( + self.BORDER, + self.BORDER + ), ( + self.BORDER+self.CORNERSIZE, + self.BORDER+self.CORNERSIZE + ) + ], [ + ( + self.WIDTH-(self.BORDER+self.CORNERSIZE), + self.HEIGHT-(self.BORDER+self.CORNERSIZE) + ), ( + self.WIDTH-self.BORDER, + self.HEIGHT-self.BORDER + ) + ], [ + ( + self.BORDER, + self.HEIGHT-(self.BORDER+self.CORNERSIZE) + ), ( + self.BORDER+self.CORNERSIZE, + self.HEIGHT-self.BORDER + ) + ], [ + ( + self.WIDTH-(self.BORDER+self.CORNERSIZE), + self.BORDER + ), ( + self.WIDTH-self.BORDER, + self.BORDER+self.CORNERSIZE + ) + ] + ] + + drawer.ellipse(corner_positions[0], **draw_parameters) + drawer.ellipse(corner_positions[1], **draw_parameters) + drawer.ellipse(corner_positions[2], **draw_parameters) + drawer.ellipse(corner_positions[3], **draw_parameters) + + # - Rectangle -: + rectangle_positions = [ + [ + ( + self.BORDER+(self.CORNERSIZE//2), + self.BORDER + ), ( + self.WIDTH-(self.BORDER+(self.CORNERSIZE//2)), + self.HEIGHT-self.BORDER + ) + ], [ + ( + self.BORDER, + self.BORDER+(self.CORNERSIZE//2) + ), ( + self.WIDTH-self.BORDER, + self.HEIGHT-(self.BORDER+(self.CORNERSIZE//2)) + ) + ] + ] + drawer.rectangle(rectangle_positions[0], **draw_parameters) + drawer.rectangle(rectangle_positions[1], **draw_parameters) + + # - Removing outline on the inside -: + clean_up_positions = [ + [ + ( + self.BORDER+(self.CORNERSIZE//2), + self.BORDER+(self.CORNERSIZE//2) + ), ( + self.WIDTH-(self.BORDER+(self.CORNERSIZE//2)), + self.HEIGHT-(self.BORDER+(self.CORNERSIZE//2)) + ) + ], [ + ( + self.BORDER+(self.CORNERSIZE/2), + self.BORDER+self.BOARDOUTLINESIZE + ), ( + self.WIDTH-(self.BORDER+(self.CORNERSIZE//2)), + self.HEIGHT-(self.BORDER+self.BOARDOUTLINESIZE) + ) + ], [ + ( + self.BORDER+self.BOARDOUTLINESIZE, + self.BORDER+(self.CORNERSIZE//2) + ), ( + self.WIDTH-(self.BORDER+self.BOARDOUTLINESIZE), + self.HEIGHT-(self.BORDER+(self.CORNERSIZE//2)) + ) + ] + ] + drawer.rectangle(clean_up_positions[0], fill=self.BOARDCOLOR) + drawer.rectangle(clean_up_positions[1], fill=self.BOARDCOLOR) + drawer.rectangle(clean_up_positions[2], fill=self.BOARDCOLOR) + + def _draw_pieces(self, drawer: ImageDraw, board: list): + for piece_x, line in enumerate(board): + for piece_y, piece in enumerate(line): + + if piece == 1: + piece_color = self.PLAYER1COLOR + outline_width = self.PIECEOUTLINESIZE + outline_color = self.PIECEOUTLINECOLOR + elif piece == 2: + piece_color = self.PLAYER2COLOR + outline_width = self.PIECEOUTLINESIZE + outline_color = self.PIECEOUTLINECOLOR + else: + piece_color = self.BACKGROUNDCOLOR + outline_width = self.EMPTYOUTLINESIZE + outline_color = self.EMPTYOUTLINECOLOR + + start_x = self.PIECESTARTX + self.PIECEGRIDSIZE[0]*piece_y + start_y = self.PIECESTARTY + self.PIECEGRIDSIZE[1]*piece_x + + position = [ + (start_x, start_y), + (start_x+self.PIECESIZE, start_y+self.PIECESIZE) + ] + + piece_parameters = { + "fill": piece_color, + "outline": outline_color, + "width": outline_width + } + drawer.ellipse(position, **piece_parameters) + + def _draw_win(self, background: Image, game: dict): + """ + Draw the bar that shows the winning pieces. + + *Parameters* + ------------ + background: Image + The image of the board. + 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] + diagonal_length = ( + (math.sqrt( + (self.PIECEGRIDSIZE[0]*4-self.GRIDBORDER-self.BORDER)**2+ + (self.PIECEGRIDSIZE[1]*4-self.GRIDBORDER-self.BORDER)**2 + ))/ + self.PIECEGRIDSIZE[0] + ) + size_ratio = self.PIECEGRIDSIZE[1]/self.PIECEGRIDSIZE[0] + diagonal_angle = math.degrees(math.atan(size_ratio)) + + if game["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) + draw_position = [ + [ + (0, 0), + (self.PIECEGRIDSIZE[0], self.PIECEGRIDSIZE[1]) + ], [ + ((self.PIECEGRIDSIZE[0]*3), 0), + (self.PIECEGRIDSIZE[0]*4, self.PIECEGRIDSIZE[1]) + ], [ + (int(self.PIECEGRIDSIZE[0]*0.5), 0), + (int(self.PIECEGRIDSIZE[0]*3.5), self.PIECEGRIDSIZE[1]) + ] + ] + win_drawer.ellipse(draw_position[0], fill=self.WINBARWHITE) + win_drawer.ellipse(draw_position[1], fill=self.WINBARWHITE) + win_drawer.rectangle(draw_position[2], fill=self.WINBARWHITE) + + elif game["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) + draw_position = [ + [ + (0, 0), + (self.PIECEGRIDSIZE[0], self.PIECEGRIDSIZE[1]) + ], [ + (0, (self.PIECEGRIDSIZE[1]*3)), + (self.PIECEGRIDSIZE[0], self.PIECEGRIDSIZE[1]*4) + ], [ + (0, (int(self.PIECEGRIDSIZE[1]*0.5))), + (self.PIECEGRIDSIZE[0], int(self.PIECEGRIDSIZE[1]*3.5)) + ] + ] + win_drawer.ellipse(draw_position[0], fill=self.WINBARWHITE) + win_drawer.ellipse(draw_position[1], fill=self.WINBARWHITE) + win_drawer.rectangle(draw_position[2], fill=self.WINBARWHITE) + + elif game["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)) + win_drawer = ImageDraw.Draw(win_bar) + draw_position = [ + [ + ( + 0, + 0 + ), ( + self.PIECEGRIDSIZE[0], + self.PIECEGRIDSIZE[1] + ) + ], [ + ( + (self.PIECEGRIDSIZE[0]*(diagonal_length-1)), + 0 + ), ( + self.PIECEGRIDSIZE[0]*diagonal_length, + self.PIECEGRIDSIZE[1] + ) + ], [ + ( + int(self.PIECEGRIDSIZE[0]*0.5), + 0 + ), ( + int(self.PIECEGRIDSIZE[0]*(diagonal_length-0.5)), + self.PIECEGRIDSIZE[1] + ) + ] + ] + win_drawer.ellipse(draw_position[0], fill=self.WINBARWHITE) + win_drawer.ellipse(draw_position[1], fill=self.WINBARWHITE) + win_drawer.rectangle(draw_position[2], fill=self.WINBARWHITE) + win_bar = win_bar.rotate(-diagonal_angle, expand=1) + start_x -= 90 + start_y -= 100 + + elif game["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)) + win_drawer = ImageDraw.Draw(win_bar) + draw_position = [ + [ + (0, 0), + (self.PIECEGRIDSIZE[0], self.PIECEGRIDSIZE[1]) + ], [ + ( + (self.PIECEGRIDSIZE[0]*(diagonal_length-1)), + 0 + ), ( + self.PIECEGRIDSIZE[0]*diagonal_length, + self.PIECEGRIDSIZE[1] + ) + ], [ + ( + int(self.PIECEGRIDSIZE[0]*0.5), + 0 + ), ( + int(self.PIECEGRIDSIZE[0]*(diagonal_length-0.5)), + self.PIECEGRIDSIZE[1] + ) + ] + ] + win_drawer.ellipse(draw_position[0], fill=self.WINBARWHITE) + win_drawer.ellipse(draw_position[1], fill=self.WINBARWHITE) + win_drawer.rectangle(draw_position[2], fill=self.WINBARWHITE) + win_bar = win_bar.rotate(diagonal_angle, expand=1) + start_x -= self.PIECEGRIDSIZE[0]*3 + 90 + start_y -= self.GRIDBORDER + 60 + + mask = win_bar.copy() + + 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": + player1 = "Gwendolyn" + else: + player1 = self.database_funcs.get_name(game["players"][0]) + + if game["players"][1] == "Gwendolyn": + player2 = "Gwendolyn" + else: + player2 = self.database_funcs.get_name(game["players"][1]) + + circle_height = self.HEIGHT - self.BORDER + circle_height += (self.BOTTOMBORDER+self.BORDER)//2 - self.TEXTSIZE//2 + text_width = self.FONT.getsize(player2)[0] + circle_right_padding = ( + self.BORDER+self.TEXTSIZE+text_width+self.TEXTPADDING + ) + circle_positions = [ + [ + ( + self.BORDER, + circle_height + ), ( + self.BORDER+self.TEXTSIZE, + circle_height+self.TEXTSIZE + ) + ], [ + ( + self.WIDTH-circle_right_padding, + circle_height + ), ( + self.WIDTH-self.BORDER-text_width-self.TEXTPADDING, + circle_height+self.TEXTSIZE + ) + ] + ] + circle_parameters = { + "outline": self.BOARDOUTLINECOLOR, + "width": 3 + } + drawer.ellipse( + circle_positions[0], + fill=self.PLAYER1COLOR, + **circle_parameters + ) + drawer.ellipse( + circle_positions[1], + fill=self.PLAYER2COLOR, + **circle_parameters + ) + + text_positions = [ + (self.BORDER+self.TEXTSIZE+self.TEXTPADDING, circle_height), + (self.WIDTH-self.BORDER-text_width, circle_height) + ] + drawer.text(text_positions[0], player1, font=self.FONT, fill=(0, 0, 0)) + drawer.text(text_positions[1], player2, font=self.FONT, fill=(0, 0, 0)) diff --git a/gwendolyn/funcs/games/games_container.py b/gwendolyn/funcs/games/games_container.py new file mode 100644 index 0000000..7c8a5b2 --- /dev/null +++ b/gwendolyn/funcs/games/games_container.py @@ -0,0 +1,48 @@ +""" +Has a container for game functions. + +*Classes* +--------- + Games + Container for game functions. +""" + + +from .invest import Invest +from .trivia import Trivia +from .blackjack import Blackjack +from .connect_four import ConnectFour +from .hangman import Hangman +from .hex import HexGame + + +class Games(): + """ + Contains game classes. + + *Attributes* + ------------ + bot: Gwendolyn + The instance of Gwendolyn. + invest + Contains investment functions. + blackjack + Contains blackjack functions. + connect_four + Contains connect four functions. + hangman + Contains hangman functions. + hex + Contains hex functions + """ + + def __init__(self, bot): + """Initialize the container.""" + self.bot = bot + + self.invest = Invest(bot) + self.trivia = Trivia(bot) + self.blackjack = Blackjack(bot) + self.connect_four = ConnectFour(bot) + self.hangman = Hangman(bot) + self.hex = HexGame(bot) diff --git a/gwendolyn/funcs/games/hangman.py b/gwendolyn/funcs/games/hangman.py new file mode 100644 index 0000000..0f247e4 --- /dev/null +++ b/gwendolyn/funcs/games/hangman.py @@ -0,0 +1,627 @@ +""" +Deals with commands and logic for hangman games. + +*Classes* +--------- + Hangman() + Deals with the game logic of hangman. + DrawHangman() + Draws the image shown to the player. +""" +import datetime # Used for generating the game id +import string # string.ascii_uppercase used +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 + +from discord_slash.context import SlashContext # Used for typehints +from PIL import ImageDraw, Image, ImageFont # Used to draw the image + + +class Hangman(): + """ + Controls hangman commands and game logic. + + *Methods* + --------- + start(ctx: SlashContext) + stop(ctx: SlashContext) + guess(message: discord.message, user: str, guess: str) + """ + + def __init__(self, bot): + """ + Initialize the class. + + *Attributes* + ------------ + draw: DrawHangman + The DrawHangman used to draw the hangman image. + API_url: str + The url to get the words from. + APIPARAMS: dict + The parameters to pass to every api call. + """ + 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"] + self.__APIPARAMS = { # pylint: disable=invalid-name + "hasDictionaryDef": True, + "minCorpusCount": 5000, + "maxCorpusCount": -1, + "minDictionaryCount": 1, + "maxDictionaryCount": -1, + "minLength": 3, + "maxLength": 11, + "limit": 1, + "api_key": api_key + } + + async def start(self, ctx: SlashContext): + """ + Start a game of hangman. + + *Parameters* + ------------ + 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 + + 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()) + + 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) + + remaining_letters = list(string.ascii_uppercase) + + self.__draw.draw_image(channel) + + 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"] + + self.__bot.log(log_message) + await ctx.send(send_message) + + 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)) + + blank_message = await ctx.channel.send("_ _") + reaction_messages = { + new_image: remaining_letters[:15], + blank_message: remaining_letters[15:] + } + + old_messages = f"{new_image.id}\n{blank_message.id}" + old_images_path = "gwendolyn/resources/games/old_images/" + + with open(old_images_path+f"hangman{channel}", "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) + + async def stop(self, ctx: SlashContext): + """ + Stop the game of hangman. + + *Parameters* + ------------ + 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/" + + with open(old_images_path+f"hangman{channel}", "r") as file_pointer: + messages = file_pointer.read().splitlines() + + for message in messages: + old_message = await ctx.channel.fetch_message(int(message)) + self.__bot.log("Deleting old message") + await old_message.delete() + + await ctx.send("Game stopped") + + async def guess(self, message: discord.Message, user: str, guess: str): + """ + Guess a letter. + + *Parameters* + ------------ + message: discord.Message + The message that the reaction was placed on. + user: str + The id of the user. + 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") + correct_guess = 0 + + for i, letter in enumerate(game["word"]): + if guess == letter: + correct_guess += 1 + updater = {"$set": {f"guessed.{i}": True}} + hangman_games.update_one({"_id": channel}, updater) + + if correct_guess == 0: + updater = {"$inc": {"misses": 1}} + hangman_games.update_one({"_id": channel}, updater) + + updater = {"$push": {"guessed letters": guess}} + hangman_games.update_one({"_id": channel}, updater) + + remaining_letters = list(string.ascii_uppercase) + + game = hangman_games.find_one({"_id": channel}) + + for letter in game["guessed letters"]: + remaining_letters.remove(letter) + + 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) + + 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() + + 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)) + + 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} + + 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(): + """ + Draws the image of the hangman game. + + *Methods* + --------- + draw_image(channel: str) + """ + + def __init__(self, bot): + """ + Initialize the class. + + *Attributes* + ------------ + CIRCLESIZE + LINEWIDTH + BODYSIZE + LIMBSIZE + ARMPOSITION + MANX, MANY + LETTERLINELENGTH + LETTERLINEDISTANCE + GALLOWX, GALLOWY + PHI + FONT + SMALLFONT + """ + self.__bot = bot + # pylint: disable=invalid-name + self.__CIRCLESIZE = 120 + self.__LINEWIDTH = 12 + + self.__BODYSIZE = 210 + self.__LIMBSIZE = 60 + self.__ARMPOSITION = 60 + + self.__MANX = (self.__LIMBSIZE*2) + self.__MANY = (self.__CIRCLESIZE+self.__BODYSIZE+self.__LIMBSIZE) + MANPADDING = self.__LINEWIDTH*4 + self.__MANX += MANPADDING + self.__MANY += MANPADDING + + self.__LETTERLINELENGTH = 90 + self.__LETTERLINEDISTANCE = 30 + + self.__GALLOWX, self.__GALLOWY = 360, 600 + self.__PHI = 1-(1 / ((1 + 5 ** 0.5) / 2)) + + LETTERSIZE = 75 # Wrong guesses letter size + WORDSIZE = 70 # Correct guesses letter size + + FONTPATH = "gwendolyn/resources/fonts/comic-sans-bold.ttf" + self.__FONT = ImageFont.truetype(FONTPATH, LETTERSIZE) + self.__SMALLFONT = ImageFont.truetype(FONTPATH, WORDSIZE) + # pylint: enable=invalid-name + + def __deviate(self, pre_deviance: int, pre_deviance_accuracy: int, + position_change: float, maxmin: int, + max_acceleration: float): + random_deviance = random.uniform(-position_change, position_change) + deviance_accuracy = pre_deviance_accuracy + random_deviance + if deviance_accuracy > maxmin * max_acceleration: + deviance_accuracy = maxmin * max_acceleration + elif deviance_accuracy < -maxmin * max_acceleration: + deviance_accuracy = -maxmin * max_acceleration + + deviance = pre_deviance + deviance_accuracy + if deviance > maxmin: + deviance = maxmin + elif deviance < -maxmin: + deviance = -maxmin + return deviance, deviance_accuracy + + def __bad_circle(self): + circle_padding = (self.__LINEWIDTH*3) + image_width = self.__CIRCLESIZE+circle_padding + image_size = (image_width, image_width) + background = Image.new("RGBA", image_size, color=(0, 0, 0, 0)) + + drawer = ImageDraw.Draw(background, "RGBA") + middle = (self.__CIRCLESIZE+(self.__LINEWIDTH*3))/2 + deviance_x = 0 + deviance_y = 0 + deviance_accuracy_x = 0 + deviance_accuracy_y = 0 + start = random.randint(-100, -80) + degreess_amount = 360 + random.randint(-10, 30) + + for degree in range(degreess_amount): + deviance_x, deviance_accuracy_x = self.__deviate( + deviance_x, + deviance_accuracy_x, + self.__LINEWIDTH/100, + self.__LINEWIDTH, + 0.03 + ) + deviance_y, deviance_accuracy_y = self.__deviate( + deviance_y, + deviance_accuracy_y, + self.__LINEWIDTH/100, + self.__LINEWIDTH, + 0.03 + ) + + radians = math.radians(degree+start) + circle_x = (math.cos(radians) * (self.__CIRCLESIZE/2)) + circle_y = (math.sin(radians) * (self.__CIRCLESIZE/2)) + + position_x = middle + circle_x - (self.__LINEWIDTH/2) + deviance_x + position_y = middle + circle_y - (self.__LINEWIDTH/2) + deviance_y + + circle_position = [ + (position_x, position_y), + (position_x+self.__LINEWIDTH, position_y+self.__LINEWIDTH) + ] + drawer.ellipse(circle_position, fill=(0, 0, 0, 255)) + + return background + + def __bad_line(self, length: int, rotated: bool = False): + if rotated: + width, height = length+self.__LINEWIDTH*3, self.__LINEWIDTH*3 + else: + width, height = self.__LINEWIDTH*3, length+self.__LINEWIDTH*3 + background = Image.new("RGBA", (width, height), color=(0, 0, 0, 0)) + + drawer = ImageDraw.Draw(background, "RGBA") + + possible_deviance = int(self.__LINEWIDTH/3) + deviance_x = random.randint(-possible_deviance, possible_deviance) + deviance_y = 0 + deviance_accuracy_x = 0 + deviance_accuracy_y = 0 + + for pixel in range(length): + deviance_x, deviance_accuracy_x = self.__deviate( + deviance_x, + deviance_accuracy_x, + self.__LINEWIDTH/1000, + self.__LINEWIDTH, + 0.004 + ) + deviance_y, deviance_accuracy_y = self.__deviate( + deviance_y, + deviance_accuracy_y, + self.__LINEWIDTH/1000, + self.__LINEWIDTH, + 0.004 + ) + + if rotated: + position_x = self.__LINEWIDTH + pixel + deviance_x + position_y = self.__LINEWIDTH + deviance_y + else: + position_x = self.__LINEWIDTH + deviance_x + position_y = self.__LINEWIDTH + pixel + deviance_y + + circle_position = [ + (position_x, position_y), + (position_x+self.__LINEWIDTH, position_y+self.__LINEWIDTH) + ] + drawer.ellipse(circle_position, fill=(0, 0, 0, 255)) + + return background + + def __draw_man(self, misses: int, seed: str): + random.seed(seed) + man_size = (self.__MANX, self.__MANY) + background = Image.new("RGBA", man_size, color=(0, 0, 0, 0)) + + if misses >= 1: + head = self.__bad_circle() + paste_x = (self.__MANX-(self.__CIRCLESIZE+(self.__LINEWIDTH*3)))//2 + paste_position = (paste_x, 0) + background.paste(head, paste_position, head) + if misses >= 2: + body = self.__bad_line(self.__BODYSIZE) + paste_x = (self.__MANX-(self.__LINEWIDTH*3))//2 + paste_position = (paste_x, self.__CIRCLESIZE) + background.paste(body, paste_position, body) + + if misses >= 3: + limbs = random.sample(["rl", "ll", "ra", "la"], min(misses-2, 4)) + else: + limbs = [] + + random.seed(seed) + + for limb in limbs: + limb_drawing = self.__bad_line(self.__LIMBSIZE, True) + x_position = (self.__MANX-(self.__LINEWIDTH*3))//2 + + if limb[1] == "a": + rotation = random.randint(-45, 45) + shift = math.sin(math.radians(rotation)) + line_length = self.__LIMBSIZE+(self.__LINEWIDTH*3) + compensation = int(shift*line_length) + limb_drawing = limb_drawing.rotate(rotation, expand=1) + y_position = self.__CIRCLESIZE + self.__ARMPOSITION + if limb == "ra": + compensation = min(-compensation, 0) + else: + x_position -= self.__LIMBSIZE + compensation = min(compensation, 0) + + y_position += compensation + else: + rotation = random.randint(-15, 15) + y_position = self.__CIRCLESIZE+self.__BODYSIZE-self.__LINEWIDTH + if limb == "rl": + limb_drawing = limb_drawing.rotate(rotation-45, expand=1) + else: + x_position += -limb_drawing.size[0]+self.__LINEWIDTH*3 + limb_drawing = limb_drawing.rotate(rotation+45, expand=1) + + paste_position = (x_position, y_position) + background.paste(limb_drawing, paste_position, limb_drawing) + + return background + + def __bad_text(self, text: str, big: bool, color: tuple = (0, 0, 0, 255)): + if big: + font = self.__FONT + else: + font = self.__SMALLFONT + width, height = font.getsize(text) + img = Image.new("RGBA", (width, height), color=(0, 0, 0, 0)) + drawer = ImageDraw.Draw(img, "RGBA") + + drawer.text((0, 0), text, font=font, fill=color) + return img + + def __draw_gallows(self): + gallow_size = (self.__GALLOWX, self.__GALLOWY) + background = Image.new("RGBA", gallow_size, color=(0, 0, 0, 0)) + + bottom_line = self.__bad_line(int(self.__GALLOWX * 0.75), True) + bottom_line_x = int(self.__GALLOWX * 0.125) + bottom_line_y = self.__GALLOWY-(self.__LINEWIDTH*4) + paste_position = (bottom_line_x, bottom_line_y) + background.paste(bottom_line, paste_position, bottom_line) + + line_two = self.__bad_line(self.__GALLOWY-self.__LINEWIDTH*6) + line_two_x = int(self.__GALLOWX*(0.75*self.__PHI)) + line_two_y = self.__LINEWIDTH*2 + paste_position = (line_two_x, line_two_y) + background.paste(line_two, paste_position, line_two) + + top_line = self.__bad_line(int(self.__GALLOWY*0.30), True) + paste_x = int(self.__GALLOWX*(0.75*self.__PHI))-self.__LINEWIDTH + paste_position = (paste_x, self.__LINEWIDTH*3) + background.paste(top_line, paste_position, top_line) + + last_line = self.__bad_line(int(self.__GALLOWY*0.125)) + paste_x += int(self.__GALLOWY*0.30) + background.paste(last_line, (paste_x, self.__LINEWIDTH*3), last_line) + return background + + def __draw_letter_lines(self, word: str, guessed: list, misses: int): + letter_width = self.__LETTERLINELENGTH+self.__LETTERLINEDISTANCE + image_width = letter_width*len(word) + image_size = (image_width, self.__LETTERLINELENGTH+self.__LINEWIDTH*3) + letter_lines = Image.new("RGBA", image_size, color=(0, 0, 0, 0)) + for i, letter in enumerate(word): + line = self.__bad_line(self.__LETTERLINELENGTH, True) + paste_x = i*(self.__LETTERLINELENGTH+self.__LETTERLINEDISTANCE) + paste_position = (paste_x, self.__LETTERLINELENGTH) + letter_lines.paste(line, paste_position, line) + if guessed[i]: + letter_drawing = self.__bad_text(letter, True) + letter_width = self.__FONT.getsize(letter)[0] + letter_x = i*(self.__LETTERLINELENGTH+self.__LETTERLINEDISTANCE) + letter_x -= (letter_width//2) + letter_x += (self.__LETTERLINELENGTH//2)+(self.__LINEWIDTH*2) + letter_lines.paste( + letter_drawing, + (letter_x, 0), + letter_drawing + ) + elif misses == 6: + letter_drawing = self.__bad_text(letter, True, (242, 66, 54)) + letter_width = self.__FONT.getsize(letter)[0] + letter_x = i*(self.__LETTERLINELENGTH+self.__LETTERLINEDISTANCE) + letter_x -= (letter_width//2) + letter_x += (self.__LETTERLINELENGTH//2)+(self.__LINEWIDTH*2) + letter_lines.paste( + letter_drawing, + (letter_x, 0), + letter_drawing + ) + + return letter_lines + + def __shortest_dist(self, positions: list, new_position: tuple): + shortest_dist = math.inf + for i, j in positions: + x_distance = abs(i-new_position[0]) + y_distance = abs(j-new_position[1]) + dist = math.sqrt(x_distance**2+y_distance**2) + if shortest_dist > dist: + shortest_dist = dist + return shortest_dist + + def __draw_misses(self, guesses: list, word: str): + background = Image.new("RGBA", (600, 400), color=(0, 0, 0, 0)) + pos = [] + for guess in guesses: + if guess not in word: + placed = False + while not placed: + letter = self.__bad_text(guess, True) + w, h = self.__FONT.getsize(guess) + x = random.randint(0, 600-w) + y = random.randint(0, 400-h) + if self.__shortest_dist(pos, (x, y)) > 70: + pos.append((x, y)) + background.paste(letter, (x, y), letter) + placed = True + return background + + def draw_image(self, channel: str): + """ + Draw a hangman Image. + + *Parameters* + ------------ + 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}) + + random.seed(game["game ID"]) + + background = Image.open("gwendolyn/resources/paper.jpg") + gallow = self.__draw_gallows() + man = self.__draw_man(game["misses"], game["game ID"]) + + random.seed(game["game ID"]) + letter_line_parameters = [game["word"], game["guessed"], game["misses"]] + letter_lines = self.__draw_letter_lines(*letter_line_parameters) + + random.seed(game["game ID"]) + misses = self.__draw_misses(game["guessed letters"], game["word"]) + + background.paste(gallow, (100, 100), gallow) + background.paste(man, (300, 210), man) + background.paste(letter_lines, (120, 840), letter_lines) + background.paste(misses, (600, 150), misses) + + misses_text = self.__bad_text("MISSES", False) + 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) diff --git a/funcs/games/hex.py b/gwendolyn/funcs/games/hex.py similarity index 75% rename from funcs/games/hex.py rename to gwendolyn/funcs/games/hex.py index 43f74d1..1f33f26 100644 --- a/funcs/games/hex.py +++ b/gwendolyn/funcs/games/hex.py @@ -28,24 +28,24 @@ class HexGame(): await ctx.send("You can't surrender when you're not a player.") else: opponent = (players.index(user) + 1) % 2 - opponentName = self.bot.databaseFuncs.getName(players[opponent]) + opponent_name = self.bot.database_funcs.get_name(players[opponent]) self.bot.database["hex games"].update_one({"_id":channel},{"$set":{"winner":opponent + 1}}) await ctx.send(f"{ctx.author.display_name} surrendered") - with open(f"resources/games/oldImages/hex{channel}", "r") as f: - oldImage = await ctx.channel.fetch_message(int(f.read())) + with open(f"gwendolyn/resources/games/old_images/hex{channel}", "r") as f: + old_image = await ctx.channel.fetch_message(int(f.read())) - if oldImage is not None: - await oldImage.delete() + if old_image is not None: + await old_image.delete() else: self.bot.log("The old image was already deleted") self.bot.log("Sending the image") - filePath = f"resources/games/hexBoards/board{channel}.png" - oldImage = await ctx.channel.send(file = discord.File(filePath)) + file_path = f"gwendolyn/resources/games/hex_boards/board{channel}.png" + old_image = await ctx.channel.send(file = discord.File(file_path)) - with open(f"resources/games/oldImages/hex{channel}", "w") as f: - f.write(str(oldImage.id)) + with open(f"gwendolyn/resources/games/old_images/hex{channel}", "w") as f: + f.write(str(old_image.id)) self.bot.database["hex games"].delete_one({"_id":channel}) @@ -71,26 +71,26 @@ class HexGame(): self.draw.drawSwap(channel) opponent = game["players"][::-1][game["turn"]-1] - gwendoTurn = (opponent == f"#{self.bot.user.id}") - opponentName = self.bot.databaseFuncs.getName(opponent) - await ctx.send(f"The color of the players were swapped. It is now {opponentName}'s turn") + gwendolyn_turn = (opponent == f"#{self.bot.user.id}") + opponent_name = self.bot.database_funcs.get_name(opponent) + await ctx.send(f"The color of the players were swapped. It is now {opponent_name}'s turn") - with open(f"resources/games/oldImages/hex{channel}", "r") as f: - oldImage = await ctx.channel.fetch_message(int(f.read())) + with open(f"gwendolyn/resources/games/old_images/hex{channel}", "r") as f: + old_image = await ctx.channel.fetch_message(int(f.read())) - if oldImage is not None: - await oldImage.delete() + if old_image is not None: + await old_image.delete() else: self.bot.log("The old image was already deleted") self.bot.log("Sending the image") - filePath = f"resources/games/hexBoards/board{channel}.png" - oldImage = await ctx.channel.send(file = discord.File(filePath)) + file_path = f"gwendolyn/resources/games/hex_boards/board{channel}.png" + old_image = await ctx.channel.send(file = discord.File(file_path)) - with open(f"resources/games/oldImages/hex{channel}", "w") as f: - f.write(str(oldImage.id)) + with open(f"gwendolyn/resources/games/old_images/hex{channel}", "w") as f: + f.write(str(old_image.id)) - if gwendoTurn: + if gwendolyn_turn: await self.hexAI(ctx) # Starts the game @@ -100,109 +100,109 @@ class HexGame(): channel = str(ctx.channel_id) game = self.bot.database["hex games"].find_one({"_id":channel}) - startedGame = False - canStart = True + started_game = False + can_start = True if game != None: - sendMessage = "There's already a hex game going on in this channel" - logMessage = "There was already a game going on" - canStart = False + send_message = "There's already a hex game going on in this channel" + log_message = "There was already a game going on" + can_start = False else: if type(opponent) == int: # Opponent is Gwendolyn if opponent in range(1, 6): - opponentName = "Gwendolyn" + opponent_name = "Gwendolyn" difficulty = int(opponent) - diffText = f" with difficulty {difficulty}" + difficulty_text = f" with difficulty {difficulty}" opponent = f"#{self.bot.user.id}" else: - sendMessage = "Difficulty doesn't exist" - logMessage = "They tried to play against a difficulty that doesn't exist" - canStart = False + send_message = "Difficulty doesn't exist" + log_message = "They tried to play against a difficulty that doesn't exist" + can_start = False elif type(opponent) == discord.member.Member: if opponent.bot: # User has challenged a bot if opponent == self.bot.user: # It was Gwendolyn - opponentName = "Gwendolyn" + opponent_name = "Gwendolyn" difficulty = 2 - diffText = f" with difficulty {difficulty}" + difficulty_text = f" with difficulty {difficulty}" opponent = f"#{self.bot.user.id}" else: - sendMessage = "You can't challenge a bot!" - logMessage = "They tried to challenge a bot" - canStart = False + send_message = "You can't challenge a bot!" + log_message = "They tried to challenge a bot" + can_start = False else: # Opponent is another player if ctx.author != opponent: - opponentName = opponent.display_name + opponent_name = opponent.display_name opponent = f"#{opponent.id}" difficulty = 5 - diffText = "" + difficulty_text = "" else: - sendMessage = "You can't play against yourself" - logMessage = "They tried to play against themself" - canStart = False + send_message = "You can't play against yourself" + log_message = "They tried to play against themself" + can_start = False else: - canStart = False - logMessage = f"Opponent was neither int or member. It was {type(opponent)}" - sendMessage = "Something went wrong" + can_start = False + log_message = f"Opponent was neither int or member. It was {type(opponent)}" + send_message = "Something went wrong" - if canStart: + if can_start: # board is 11x11 board = [[0 for i in range(self.BOARDWIDTH)] for j in range(self.BOARDWIDTH)] players = [user, opponent] random.shuffle(players) # random starting player gameHistory = [] - newGame = {"_id":channel,"board":board, "winner":0, + new_game = {"_id":channel,"board":board, "winner":0, "players":players, "turn":1, "difficulty":difficulty, "gameHistory":gameHistory} - self.bot.database["hex games"].insert_one(newGame) + self.bot.database["hex games"].insert_one(new_game) # draw the board self.draw.drawBoard(channel) - gwendoTurn = (players[0] == f"#{self.bot.user.id}") - startedGame = True + gwendolyn_turn = (players[0] == f"#{self.bot.user.id}") + started_game = True - turnName = self.bot.databaseFuncs.getName(players[0]) - sendMessage = f"Started Hex game against {opponentName}{diffText}. It's {turnName}'s turn" - logMessage = "Game started" + turn_name = self.bot.database_funcs.get_name(players[0]) + send_message = f"Started Hex game against {opponent_name}{difficulty_text}. It's {turn_name}'s turn" + log_message = "Game started" - await ctx.send(sendMessage) - self.bot.log(logMessage) + await ctx.send(send_message) + self.bot.log(log_message) - if startedGame: - filePath = f"resources/games/hexBoards/board{ctx.channel_id}.png" - newImage = await ctx.channel.send(file = discord.File(filePath)) + if started_game: + file_path = f"gwendolyn/resources/games/hex_boards/board{ctx.channel_id}.png" + new_image = await ctx.channel.send(file = discord.File(file_path)) - with open(f"resources/games/oldImages/hex{ctx.channel_id}", "w") as f: - f.write(str(newImage.id)) + with open(f"gwendolyn/resources/games/old_images/hex{ctx.channel_id}", "w") as f: + f.write(str(new_image.id)) - if gwendoTurn: + if gwendolyn_turn: await self.hexAI(ctx) # Places a piece at the given location and checks things afterwards async def placeHex(self, ctx, position : str, user): channel = str(ctx.channel_id) game = self.bot.database["hex games"].find_one({"_id":channel}) - placedPiece = False + placed_piece = False if game == None: - sendMessage = "There's no game in this channel" + send_message = "There's no game in this channel" self.bot.log("There was no game going on") elif not (position[0].isalpha() and position[1:].isnumeric() and len(position) in [2, 3]): - sendMessage = "The position must be a letter followed by a number." + send_message = "The position must be a letter followed by a number." self.bot.log(f"The position was not valid, {position}") else: players = game["players"] if user not in players: - sendMessage = f"You can't place when you're not in the game. The game's players are: {self.bot.databaseFuncs.getName(game['players'][0])} and {self.bot.databaseFuncs.getName(game['players'][1])}." + send_message = f"You can't place when you're not in the game. The game's players are: {self.bot.database_funcs.get_name(game['players'][0])} and {self.bot.database_funcs.get_name(game['players'][1])}." self.bot.log("They aren't in the game") elif players[game["turn"]-1] != user: - sendMessage = "It's not your turn" + send_message = "It's not your turn" self.bot.log("It wasn't their turn") else: player = game["turn"] @@ -215,7 +215,7 @@ class HexGame(): if board is None: self.bot.log("It was an invalid position") - sendMessage = ("That's an invalid position. You must place your piece on an empty field.") + send_message = ("That's an invalid position. You must place your piece on an empty field.") else: # If the move is valid: self.bot.database["hex games"].update_one({"_id":channel},{"$set":{"board":board}}) @@ -227,50 +227,50 @@ class HexGame(): winner = self.evaluateBoard(game["board"])[1] if winner == 0: # Continue with the game. - gameWon = False - sendMessage = self.bot.databaseFuncs.getName(game["players"][player-1])+" placed at "+position.upper()+". It's now "+self.bot.databaseFuncs.getName(game["players"][turn-1])+"'s turn."# The score is "+str(score) + game_won = False + send_message = self.bot.database_funcs.get_name(game["players"][player-1])+" placed at "+position.upper()+". It's now "+self.bot.database_funcs.get_name(game["players"][turn-1])+"'s turn."# The score is "+str(score) else: # Congratulations! - gameWon = True + game_won = True self.bot.database["hex games"].update_one({"_id":channel},{"$set":{"winner":winner}}) - sendMessage = self.bot.databaseFuncs.getName(game["players"][player-1])+" placed at "+position.upper()+" and won!" + send_message = self.bot.database_funcs.get_name(game["players"][player-1])+" placed at "+position.upper()+" and won!" if game["players"][winner-1] != f"#{self.bot.user.id}": - winAmount = game["difficulty"]*10 - sendMessage += " Adding "+str(winAmount)+" GwendoBucks to their account." + win_amount = game["difficulty"]*10 + send_message += " Adding "+str(win_amount)+" GwendoBucks to their account." self.bot.database["hex games"].update_one({"_id":channel}, {"$push":{"gameHistory":(int(position[1])-1, ord(position[0])-97)}}) # Is it now Gwendolyn's turn? - gwendoTurn = False + gwendolyn_turn = False if game["players"][turn-1] == f"#{self.bot.user.id}": self.bot.log("It's Gwendolyn's turn") - gwendoTurn = True + gwendolyn_turn = True - placedPiece = True + placed_piece = True if user == f"#{self.bot.user.id}": - await ctx.channel.send(sendMessage) + await ctx.channel.send(send_message) else: - await ctx.send(sendMessage) + await ctx.send(send_message) - if placedPiece: + if placed_piece: # Update the board self.draw.drawHexPlacement(channel,player, position) - with open(f"resources/games/oldImages/hex{channel}", "r") as f: - oldImage = await ctx.channel.fetch_message(int(f.read())) + with open(f"gwendolyn/resources/games/old_images/hex{channel}", "r") as f: + old_image = await ctx.channel.fetch_message(int(f.read())) - if oldImage is not None: - await oldImage.delete() + if old_image is not None: + await old_image.delete() else: self.bot.log("The old image was already deleted") self.bot.log("Sending the image") - filePath = f"resources/games/hexBoards/board{channel}.png" - oldImage = await ctx.channel.send(file = discord.File(filePath)) + file_path = f"gwendolyn/resources/games/hex_boards/board{channel}.png" + old_image = await ctx.channel.send(file = discord.File(file_path)) - if gameWon: + if game_won: self.bot.log("Dealing with the winning player") game = self.bot.database["hex games"].find_one({"_id":channel}) @@ -281,10 +281,10 @@ class HexGame(): self.bot.database["hex games"].delete_one({"_id":channel}) else: - with open(f"resources/games/oldImages/hex{channel}", "w") as f: - f.write(str(oldImage.id)) + with open(f"gwendolyn/resources/games/old_images/hex{channel}", "w") as f: + f.write(str(old_image.id)) - if gwendoTurn: + if gwendolyn_turn: await self.hexAI(ctx) # Returns a board where the placement has ocurred @@ -314,14 +314,14 @@ class HexGame(): game = self.bot.database["hex games"].find_one({"_id":channel}) if user not in game["players"]: - sendMessage = "You're not a player in the game" + send_message = "You're not a player in the game" elif len(game["gameHistory"]) == 0: - sendMessage = "You can't undo nothing" + send_message = "You can't undo nothing" elif user != game["players"][(game["turn"] % 2)]: # If it's not your turn - sendMessage = "It's not your turn" + send_message = "It's not your turn" else: turn = game["turn"] - self.bot.log("Undoing {}'s last move".format(self.bot.databaseFuncs.getName(user))) + self.bot.log("Undoing {}'s last move".format(self.bot.database_funcs.get_name(user))) lastMove = game["gameHistory"].pop() game["board"][lastMove[0]][lastMove[1]] = 0 @@ -332,25 +332,25 @@ class HexGame(): # Update the board self.draw.drawHexPlacement(channel,0,"abcdefghijk"[lastMove[1]]+str(lastMove[0]+1)) # The zero makes the hex disappear - sendMessage = f"You undid your last move at {lastMove}" + send_message = f"You undid your last move at {lastMove}" undid = True - await ctx.send(sendMessage) + await ctx.send(send_message) if undid: - with open(f"resources/games/oldImages/hex{channel}", "r") as f: - oldImage = await ctx.channel.fetch_message(int(f.read())) + with open(f"gwendolyn/resources/games/old_images/hex{channel}", "r") as f: + old_image = await ctx.channel.fetch_message(int(f.read())) - if oldImage is not None: - await oldImage.delete() + if old_image is not None: + await old_image.delete() else: self.bot.log("The old image was already deleted") self.bot.log("Sending the image") - filePath = f"resources/games/hexBoards/board{channel}.png" - oldImage = await ctx.channel.send(file = discord.File(filePath)) + file_path = f"gwendolyn/resources/games/hex_boards/board{channel}.png" + old_image = await ctx.channel.send(file = discord.File(file_path)) - with open(f"resources/games/oldImages/hex{channel}", "w") as f: - f.write(str(oldImage.id)) + with open(f"gwendolyn/resources/games/old_images/hex{channel}", "w") as f: + f.write(str(old_image.id)) # Plays as the AI @@ -417,20 +417,20 @@ class HexGame(): return scores[2]-scores[1], winner - def minimaxHex(self, board, depth, alpha, beta, maximizingPlayer): + def minimaxHex(self, board, depth, alpha, beta, maximizing_player): # The depth is how many moves ahead the computer checks. This value is the difficulty. if depth == 0 or 0 not in sum(board,[]): score = self.evaluateBoard(board)[0] return score # if final depth is not reached, look another move ahead: - if maximizingPlayer: # red player predicts next move + if maximizing_player: # red player predicts next move maxEval = -math.inf possiblePlaces = [i for i,v in enumerate(sum(board,[])) if v == 0] #self.bot.log("Judging a red move at depth {}".format(depth)) for i in possiblePlaces: - testBoard = copy.deepcopy(board) - testBoard[i // self.BOARDWIDTH][i % self.BOARDWIDTH] = 1 # because maximizingPlayer is Red which is number 1 - evaluation = self.minimaxHex(testBoard,depth-1,alpha,beta,False) + test_board = copy.deepcopy(board) + test_board[i // self.BOARDWIDTH][i % self.BOARDWIDTH] = 1 # because maximizing_player is Red which is number 1 + evaluation = self.minimaxHex(test_board,depth-1,alpha,beta,False) maxEval = max(maxEval, evaluation) alpha = max(alpha, evaluation) if beta <= alpha: @@ -442,9 +442,9 @@ class HexGame(): possiblePlaces = [i for i,v in enumerate(sum(board,[])) if v == 0] #self.bot.log("Judging a blue move at depth {}".format(depth)) for i in possiblePlaces: - testBoard = copy.deepcopy(board) - testBoard[i // self.BOARDWIDTH][i % self.BOARDWIDTH] = 2 # because minimizingPlayer is Blue which is number 2 - evaluation = self.minimaxHex(testBoard,depth-1,alpha,beta,True) + test_board = copy.deepcopy(board) + test_board[i // self.BOARDWIDTH][i % self.BOARDWIDTH] = 2 # because minimizingPlayer is Blue which is number 2 + evaluation = self.minimaxHex(test_board,depth-1,alpha,beta,True) minEval = min(minEval, evaluation) beta = min(beta, evaluation) if beta <= alpha: @@ -474,7 +474,7 @@ class DrawHex(): self.HEXAGONHEIGHT = 1.5 * self.SIDELENGTH self.FONTSIZE = 45 self.TEXTCOLOR = (0,0,0) - self.FONT = ImageFont.truetype('resources/fonts/futura-bold.ttf', self.FONTSIZE) + self.FONT = ImageFont.truetype('gwendolyn/resources/fonts/futura-bold.ttf', self.FONTSIZE) self.LINETHICKNESS = 15 self.HEXTHICKNESS = 6 # This is half the width of the background lining between every hex @@ -490,7 +490,7 @@ class DrawHex(): self.COLYTHICKNESS = self.COLHEXTHICKNESS * math.sin(math.pi/6) # The Name display things: self.NAMESIZE = 60 - self.NAMEFONT = ImageFont.truetype('resources/fonts/futura-bold.ttf', self.NAMESIZE) + self.NAMEFONT = ImageFont.truetype('gwendolyn/resources/fonts/futura-bold.ttf', self.NAMESIZE) self.XNAME = {1:175, 2:self.CANVASWIDTH-100} self.YNAME = {1:self.CANVASHEIGHT-150, 2:150} self.NAMEHEXPADDING = 90 @@ -556,7 +556,7 @@ class DrawHex(): game = self.bot.database["hex games"].find_one({"_id":channel}) for p in [1,2]: - playername = self.bot.databaseFuncs.getName(game["players"][p-1]) + playername = self.bot.database_funcs.get_name(game["players"][p-1]) # Draw name x = self.XNAME[p] x -= self.NAMEFONT.getsize(playername)[0] if p==2 else 0 # player2's name is right-aligned @@ -573,13 +573,13 @@ class DrawHex(): (x, y+self.SMALLSIDELENGTH), ],fill = self.PIECECOLOR[p]) - im.save("resources/games/hexBoards/board"+channel+".png") + im.save("gwendolyn/resources/games/hex_boards/board"+channel+".png") def drawHexPlacement(self, channel,player,position): - FILEPATH = "resources/games/hexBoards/board"+channel+".png" + FILEPATH = "gwendolyn/resources/games/hex_boards/board"+channel+".png" self.bot.log(f"Drawing a newly placed hex. Filename: board{channel}.png") # Translates position @@ -589,7 +589,7 @@ class DrawHex(): row = int(position[1:])-1 # Find the coordinates for the filled hex drawing - hexCoords = [ + hex_coords = [ (self.BOARDCOORDINATES[row][column][0]+self.COLXTHICKNESS, self.BOARDCOORDINATES[row][column][1] + self.COLYTHICKNESS), (self.BOARDCOORDINATES[row][column][0]+self.HEXAGONWIDTH/2, self.BOARDCOORDINATES[row][column][1]-0.5*self.SIDELENGTH + self.COLHEXTHICKNESS), (self.BOARDCOORDINATES[row][column][0]+self.HEXAGONWIDTH-self.COLXTHICKNESS, self.BOARDCOORDINATES[row][column][1] + self.COLYTHICKNESS), @@ -603,7 +603,7 @@ class DrawHex(): with Image.open(FILEPATH) as im: d = ImageDraw.Draw(im,"RGBA") # Draws the hex piece - d.polygon(hexCoords,fill = self.PIECECOLOR[player], outline = self.BETWEENCOLOR) + d.polygon(hex_coords,fill = self.PIECECOLOR[player], outline = self.BETWEENCOLOR) # Save im.save(FILEPATH) @@ -611,7 +611,7 @@ class DrawHex(): self.bot.log("Error drawing new hex on board (error code 1541") def drawSwap(self, channel): - FILEPATH = "resources/games/hexBoards/board"+channel+".png" + FILEPATH = "gwendolyn/resources/games/hex_boards/board"+channel+".png" game = self.bot.database["hex games"].find_one({"_id":channel}) # Opens the image try: @@ -620,7 +620,7 @@ class DrawHex(): # Write player names and color for p in [1,2]: - playername = self.bot.databaseFuncs.getName(game["players"][p%2]) + playername = self.bot.database_funcs.get_name(game["players"][p%2]) x = self.XNAME[p] x -= self.NAMEFONT.getsize(playername)[0] if p==2 else 0 # player2's name is right-aligned diff --git a/gwendolyn/funcs/games/invest.py b/gwendolyn/funcs/games/invest.py new file mode 100644 index 0000000..eaac8f0 --- /dev/null +++ b/gwendolyn/funcs/games/invest.py @@ -0,0 +1,286 @@ +""" +Contains functions relating to invest commands. + +*Classes* +--------- + Invest + Contains all the code for the invest commands. +""" +import discord # Used for embeds +from discord_slash.context import SlashContext # Used for type hints + + +class Invest(): + """ + Contains all the invest functions. + + *Methods* + --------- + getPrice(symbol: str) -> int + getPortfolio(user: str) -> str + buyStock(user: str, stock: str: buyAmount: int) -> str + sellStock(user: str, stock: str, buyAmount: int) -> str + parseInvest(ctx: discord_slash.context.SlashContext, + parameters: str) + """ + + def __init__(self, bot): + """Initialize the class.""" + self.bot = bot + + def getPrice(self, symbol: str): + """ + Get the price of a stock. + + *Parameters* + ------------ + symbol: str + The symbol of the stock to get the price of. + + *Returns* + --------- + price: int + The price of the stock. + """ + res = self.bot.finnhub_client.quote(symbol.upper()) + if res == {}: + return 0 + else: + return int(res["c"] * 100) + + def getPortfolio(self, user: str): + """ + Get the stock portfolio of a user. + + *Parameters* + ------------ + user: str + The id of the user to get the portfolio of. + + *Returns* + --------- + portfolio: str + The portfolio. + """ + investments_database = self.bot.database["investments"] + userInvestments = investments_database.find_one({"_id": user}) + + user_name = self.bot.database_funcs.get_name(user) + + if userInvestments in [None, {}]: + return f"{user_name} does not have a stock portfolio." + else: + portfolio = f"**Stock portfolio for {user_name}**" + + for key, value in list(userInvestments["investments"].items()): + purchaseValue = value["purchased for"] + stockPrice = self.getPrice(key) + valueAtPurchase = value["value at purchase"] + purchasedStock = value["purchased"] + valueChange = (stockPrice / valueAtPurchase) + currentValue = int(valueChange * purchasedStock) + portfolio += f"\n**{key}**: ___{currentValue} GwendoBucks___" + + if purchaseValue != "?": + portfolio += f" (purchased for {purchaseValue})" + + return portfolio + + def buyStock(self, user: str, stock: str, buyAmount: int): + """ + Buy an amount of a specific stock. + + *Paramaters* + ------------ + user: str + The id of the user buying. + stock: str + The symbol of the stock to buy. + buyAmount: int + The amount of GwendoBucks to use to buy the stock. + + *Returns* + --------- + send_message: str + The message to return to the user. + """ + if buyAmount < 100: + return "You cannot buy stocks for less than 100 GwendoBucks" + elif self.bot.money.checkBalance(user) < buyAmount: + return "You don't have enough money for that" + elif self.getPrice(stock) <= 0: + return f"{stock} is not traded on the american market." + else: + investments_database = self.bot.database["investments"] + stockPrice = self.getPrice(stock) + userInvestments = investments_database.find_one({"_id": user}) + + self.bot.money.addMoney(user, -1*buyAmount) + stock = stock.upper() + + if userInvestments is not None: + userInvestments = userInvestments["investments"] + if stock in userInvestments: + value = userInvestments[stock] + valueChange = (stockPrice / value["value at purchase"]) + currentValue = int(valueChange * value["purchased"]) + newAmount = currentValue + buyAmount + + value_path = f"investments.{stock}.value at purchase" + updater = {"$set": {value_path: stockPrice}} + investments_database.update_one({"_id": user}, updater) + + purchased_path = f"investments.{stock}.purchased" + updater = {"$set": {purchased_path: newAmount}} + investments_database.update_one({"_id": user}, updater) + + if value["purchased for"] != "?": + purchasedFor_path = f"investments.{stock}.purchased for" + updater = {"$set": {purchasedFor_path: buyAmount}} + investments_database.update_one({"_id": user}, updater) + else: + updater = { + "$set": { + "investments.{stock}": { + "purchased": buyAmount, + "value at purchase": stockPrice, + "purchased for": buyAmount + } + } + } + investments_database.update_one({"_id": user}, updater) + else: + new_user = { + "_id": user, + "investments": { + stock: { + "purchased": buyAmount, + "value at purchase": stockPrice, + "purchased for": buyAmount + } + } + } + investments_database.insert_one(new_user) + + user_name = self.bot.database_funcs.get_name(user) + send_message = "{} bought {} GwendoBucks worth of {} stock" + send_message = send_message.format(user_name, buyAmount, stock) + return send_message + + def sellStock(self, user: str, stock: str, sellAmount: int): + """ + Sell an amount of a specific stock. + + *Paramaters* + ------------ + user: str + The id of the user selling. + stock: str + The symbol of the stock to sell. + buyAmount: int + The amount of GwendoBucks to sell for. + + *Returns* + --------- + send_message: str + The message to return to the user. + """ + if sellAmount <= 0: + return "no" + else: + investments_database = self.bot.database["investments"] + user_data = investments_database.find_one({"_id": user}) + userInvestments = user_data["investments"] + + stock = stock.upper() + + if userInvestments is not None and stock in userInvestments: + value = userInvestments[stock] + stockPrice = self.getPrice(stock) + priceChange = (stockPrice / value["value at purchase"]) + purchasedAmount = int(priceChange * value["purchased"]) + purchased_path = f"investments.{stock}.purchased" + updater = {"$set": {purchased_path: purchasedAmount}} + investments_database.update_one({"_id": user}, updater) + valueAtPurchase_path = f"investments.{stock}.value at purchase" + updater = {"$set": {valueAtPurchase_path: stockPrice}} + investments_database.update_one({"_id": user}, updater) + if value["purchased"] >= sellAmount: + self.bot.money.addMoney(user, sellAmount) + if sellAmount < value["purchased"]: + purchased_path = f"investments.{stock}.purchased" + updater = {"$inc": {purchased_path: -sellAmount}} + investments_database.update_one({"_id": user}, updater) + + purchasedFor_path = f"investments.{stock}.purchased for" + updater = {"$set": {purchasedFor_path: "?"}} + investments_database.update_one({"_id": user}, updater) + else: + updater = {"$unset": {f"investments.{stock}": ""}} + investments_database.update_one({"_id": user}, updater) + + user_name = self.bot.database_funcs.get_name(user) + send_message = "{} sold {} GwendoBucks worth of {} stock" + return send_message.format(user_name, sellAmount, stock) + else: + return f"You don't have enough {stock} stocks to do that" + else: + return f"You don't have any {stock} stock" + + async def parseInvest(self, ctx: SlashContext, parameters: str): + """ + Parse an invest command. TO BE DELETED. + + *Parameters* + ------------ + ctx: discord_slash.context.SlashContext + The context of the slash command. + parameters: str + The parameters of the command. + """ + await self.bot.defer(ctx) + user = f"#{ctx.author.id}" + + if parameters.startswith("check"): + commands = parameters.split(" ") + if len(commands) == 1: + response = self.getPortfolio(user) + else: + price = self.getPrice(commands[1]) + if price == 0: + response = "{} is not traded on the american market." + response = response.format(commands[0].upper()) + else: + price = f"{price:,}".replace(",", ".") + response = self.bot.long_strings["Stock value"] + response = response.format(commands[1].upper(), price) + + elif parameters.startswith("buy"): + commands = parameters.split(" ") + if len(commands) == 3: + response = self.buyStock(user, commands[1], int(commands[2])) + else: + response = self.bot.long_strings["Stock parameters"] + + elif parameters.startswith("sell"): + commands = parameters.split(" ") + if len(commands) == 3: + response = self.sellStock(user, commands[1], int(commands[2])) + else: + response = self.bot.long_strings["Stock parameters"] + + else: + response = "Incorrect parameters" + + if response.startswith("**"): + responses = response.split("\n") + text = "\n".join(responses[1:]) + embedParams = { + "title": responses[0], + "description": text, + "colour": 0x00FF00 + } + em = discord.Embed(**embedParams) + await ctx.send(embed=em) + else: + await ctx.send(response) diff --git a/gwendolyn/funcs/games/money.py b/gwendolyn/funcs/games/money.py new file mode 100644 index 0000000..e46583d --- /dev/null +++ b/gwendolyn/funcs/games/money.py @@ -0,0 +1,151 @@ +""" +Contains the code that deals with money. + +*Classes* +--------- + Money + Deals with money. +""" +import discord_slash # Used for typehints +import discord # Used for typehints + + +class Money(): + """ + Deals with money. + + *Methods* + --------- + checkBalance(user: str) + sendBalance(ctx: discord_slash.context.SlashContext) + addMoney(user: str, amount: int) + giveMoney(ctx: discord_slash.context.SlashContext, user: discord.User, + amount: int) + + *Attributes* + ------------ + bot: Gwendolyn + The instance of Gwendolyn + database: pymongo.Client + The mongo database + """ + + def __init__(self, bot): + """Initialize the class.""" + self.bot = bot + self.database = bot.database + + def checkBalance(self, user: str): + """ + Get the account balance of a user. + + *Parameters* + ------------ + user: str + The user to get the balance of. + + *Returns* + --------- + balance: int + The balance of the user's account. + """ + self.bot.log("checking "+user+"'s account balance") + + user_data = self.database["users"].find_one({"_id": user}) + + if user_data is not None: + return user_data["money"] + else: + return 0 + + async def sendBalance(self, ctx: discord_slash.context.SlashContext): + """ + Get your own account balance. + + *Parameters* + ------------ + ctx: discord_slash.context.SlashContext + The context of the command. + """ + await self.bot.defer(ctx) + response = self.checkBalance("#"+str(ctx.author.id)) + user_name = ctx.author.display_name + if response == 1: + new_message = f"{user_name} has {response} GwendoBuck" + else: + new_message = f"{user_name} has {response} GwendoBucks" + await ctx.send(new_message) + + # Adds money to the account of a user + def addMoney(self, user: str, amount: int): + """ + Add money to a user account. + + *Parameters* + ------------ + user: str + The id of the user to give money. + amount: int + The amount to add to the user's account. + """ + self.bot.log("adding "+str(amount)+" to "+user+"'s account") + + user_data = self.database["users"].find_one({"_id": user}) + + if user_data is not None: + updater = {"$inc": {"money": amount}} + self.database["users"].update_one({"_id": user}, updater) + else: + new_user = { + "_id": user, + "user name": self.bot.database_funcs.get_name(user), + "money": amount + } + self.database["users"].insert_one(new_user) + + # Transfers money from one user to another + async def giveMoney(self, ctx: discord_slash.context.SlashContext, + user: discord.User, amount: int): + """ + Give someone else money from your account. + + *Parameters* + ------------ + ctx: discord_slash.context.SlashContext + The context of the command. + user: discord.User + The user to give money. + amount: int + The amount to transfer. + """ + await self.bot.defer(ctx) + username = user.display_name + if self.bot.database_funcs.get_id(username) is None: + async for member in ctx.guild.fetch_members(limit=None): + if member.display_name.lower() == username.lower(): + username = member.display_name + user_id = f"#{member.id}" + new_user = { + "_id": user_id, + "user name": username, + "money": 0 + } + self.bot.database["users"].insert_one(new_user) + + userid = f"#{ctx.author.id}" + user_data = self.database["users"].find_one({"_id": userid}) + targetUser = self.bot.database_funcs.get_id(username) + + if amount <= 0: + self.bot.log("They tried to steal") + await ctx.send("Yeah, no. You can't do that") + elif targetUser is None: + self.bot.log("They weren't in the system") + await ctx.send("The target doesn't exist") + elif user_data is None or user_data["money"] < amount: + self.bot.log("They didn't have enough GwendoBucks") + await ctx.send("You don't have that many GwendoBuck") + else: + self.addMoney(f"#{ctx.author.id}", -1 * amount) + self.addMoney(targetUser, amount) + await ctx.send(f"Transferred {amount} GwendoBucks to {username}") diff --git a/gwendolyn/funcs/games/trivia.py b/gwendolyn/funcs/games/trivia.py new file mode 100644 index 0000000..e0d2284 --- /dev/null +++ b/gwendolyn/funcs/games/trivia.py @@ -0,0 +1,205 @@ +""" +Contains code for trivia games. + +*Classes* +--------- + Trivia + Contains all the code for the trivia commands. +""" +import urllib # Used to get data from api +import json # Used to read data from api +import random # Used to shuffle answers +import asyncio # Used to sleep + +from discord_slash.context import SlashContext # Used for type hints + + +class Trivia(): + """ + Contains the code for trivia games. + + *Methods* + --------- + triviaStart(channel: str) -> str, str, str + triviaAnswer(user: str, channel: str, command: str) -> str + triviaCountPoints(channel: str) + triviaParse(ctx: SlashContext, answer: str) + """ + + def __init__(self, bot): + """Initialize the class.""" + self.bot = bot + + def triviaStart(self, channel: str): + """ + Start a game of trivia. + + Downloads a question with answers, shuffles the wrong answers + with the correct answer and returns the questions and answers. + Also saves the question in the database. + + *Parameters* + ------------ + channel: str + The id of the channel to start the game in + + *Returns* + --------- + send_message: str + The message to return to the user. + """ + triviaQuestions = self.bot.database["trivia questions"] + question = triviaQuestions.find_one({"_id": channel}) + + self.bot.log(f"Trying to find a trivia question for {channel}") + + if question is None: + apiUrl = "https://opentdb.com/api.php?amount=10&type=multiple" + with urllib.request.urlopen(apiUrl) as response: + data = json.loads(response.read()) + + question = data["results"][0]["question"] + self.bot.log(f"Found the question \"{question}\"") + answers = data["results"][0]["incorrect_answers"] + answers.append(data["results"][0]["correct_answer"]) + random.shuffle(answers) + correctAnswer = data["results"][0]["correct_answer"] + correctAnswer = answers.index(correctAnswer) + 97 + + newQuestion = { + "_id": channel, + "answer": str(chr(correctAnswer)), + "players": {} + } + triviaQuestions.insert_one(newQuestion) + + replacements = { + "'": "\'", + """: "\"", + "“": "\"", + "”": "\"", + "é": "é" + } + question = data["results"][0]["question"] + + for key, value in replacements.items(): + question = question.replace(key, value) + + for answer in answers: + for key, value in replacements.items(): + answer = answer.replace(key, value) + + return question, answers, correctAnswer + else: + log_message = "There was already a trivia question for that channel" + self.bot.log(log_message) + return self.bot.long_strings["Trivia going on"], "", "" + + def triviaAnswer(self, user: str, channel: str, command: str): + """ + Answer the current trivia question. + + *Parameters* + ------------ + user: str + The id of the user who answered. + channel: str + The id of the channel the game is in. + command: str + The user's answer. + + *Returns* + --------- + send_message: str + The message to send if the function failed. + """ + triviaQuestions = self.bot.database["trivia questions"] + question = triviaQuestions.find_one({"_id": channel}) + + if command not in ["a", "b", "c", "d"]: + self.bot.log("I didn't quite understand that") + return "I didn't quite understand that" + elif question is None: + self.bot.log("There's no question right now") + return "There's no question right now" + elif user in question["players"]: + self.bot.log(f"{user} has already answered this question") + return f"{user} has already answered this question" + else: + self.bot.log(f"{user} answered the question in {channel}") + + updater = {"$set": {f"players.{user}": command}} + triviaQuestions.update_one({"_id": channel}, updater) + return None + + def triviaCountPoints(self, channel: str): + """ + Add money to every winner's account. + + *Parameters* + ------------ + channel: str + The id of the channel the game is in. + """ + triviaQuestions = self.bot.database["trivia questions"] + question = triviaQuestions.find_one({"_id": channel}) + + self.bot.log("Counting points for question in "+channel) + + if question is not None: + for player, answer in question["players"].items(): + if answer == question["answer"]: + self.bot.money.addMoney(player, 1) + else: + self.bot.log("Couldn't find the questio") + + return None + + async def triviaParse(self, ctx: SlashContext, answer: str): + """ + Parse a trivia command. + + *Parameters* + ------------ + ctx: SlashContext + The context of the command. + answer: str + The answer, if any. + """ + await self.bot.defer(ctx) + channelId = str(ctx.channel_id) + if answer == "": + question, options, correctAnswer = self.triviaStart(channelId) + if options != "": + results = "**"+question+"**\n" + for x, option in enumerate(options): + results += chr(x+97) + ") "+option+"\n" + + await ctx.send(results) + + await asyncio.sleep(60) + + self.triviaCountPoints(channelId) + + delete_gameParams = ["trivia questions", channelId] + self.bot.database_funcs.delete_game(*delete_gameParams) + + self.bot.log("Time's up for the trivia question", channelId) + send_message = self.bot.long_strings["Trivia time up"] + format_parameters = [chr(correctAnswer), options[correctAnswer-97]] + send_message = send_message.format(*format_parameters) + await ctx.send(send_message) + else: + await ctx.send(question, hidden=True) + + elif answer in ["a", "b", "c", "d"]: + userId = f"#{ctx.author.id}" + response = self.triviaAnswer(userId, channelId, answer) + if response is None: + user_name = ctx.author.display_name + await ctx.send(f"{user_name} answered **{answer}**") + else: + await ctx.send(response) + else: + self.bot.log("I didn't understand that", channelId) + await ctx.send("I didn't understand that") diff --git a/funcs/lookup/__init__.py b/gwendolyn/funcs/lookup/__init__.py similarity index 67% rename from funcs/lookup/__init__.py rename to gwendolyn/funcs/lookup/__init__.py index 2216bf2..2329a0a 100644 --- a/funcs/lookup/__init__.py +++ b/gwendolyn/funcs/lookup/__init__.py @@ -2,4 +2,4 @@ __all__ = ["LookupFuncs"] -from .lookupFuncs import LookupFuncs \ No newline at end of file +from .lookup_funcs import LookupFuncs \ No newline at end of file diff --git a/funcs/lookup/lookupFuncs.py b/gwendolyn/funcs/lookup/lookup_funcs.py similarity index 91% rename from funcs/lookup/lookupFuncs.py rename to gwendolyn/funcs/lookup/lookup_funcs.py index 7d0654c..418ed32 100644 --- a/funcs/lookup/lookupFuncs.py +++ b/gwendolyn/funcs/lookup/lookup_funcs.py @@ -2,7 +2,7 @@ import math import json import discord -from utils import cap +from gwendolyn.utils import cap class LookupFuncs(): @@ -29,7 +29,7 @@ class LookupFuncs(): await ctx.send("I don't know that monster...") else: # Opens "monsters.json" - data = json.load(open('resources/lookup/monsters.json', encoding = "utf8")) + 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!") @@ -149,16 +149,16 @@ class LookupFuncs(): self.bot.log("Looking up "+query) # Opens "spells.json" - data = json.load(open('resources/lookup/spells.json', encoding = "utf8")) + data = json.load(open('gwendolyn/resources/lookup/spells.json', encoding = "utf8")) if query in data: self.bot.log("Returning spell information") - sendMessage = (f"***{query}***\n*{data[query]['level']} level {data[query]['school']}\nCasting Time: {data[query]['casting_time']}\nRange:{data[query]['range']}\nComponents:{data[query]['components']}\nDuration:{data[query]['duration']}*\n \n{data[query]['description']}") + send_message = (f"***{query}***\n*{data[query]['level']} level {data[query]['school']}\nCasting Time: {data[query]['casting_time']}\nRange:{data[query]['range']}\nComponents:{data[query]['components']}\nDuration:{data[query]['duration']}*\n \n{data[query]['description']}") else: self.bot.log("I don't know that spell (error code 501)") - sendMessage = "I don't think that's a spell (error code 501)" + send_message = "I don't think that's a spell (error code 501)" - if len(sendMessage) > 2000: - await ctx.send(sendMessage[:2000]) - await ctx.send(sendMessage[2000:]) + if len(send_message) > 2000: + await ctx.send(send_message[:2000]) + await ctx.send(send_message[2000:]) else: - await ctx.send(sendMessage) + await ctx.send(send_message) diff --git a/funcs/other/__init__.py b/gwendolyn/funcs/other/__init__.py similarity index 70% rename from funcs/other/__init__.py rename to gwendolyn/funcs/other/__init__.py index 8612103..9cef1df 100644 --- a/funcs/other/__init__.py +++ b/gwendolyn/funcs/other/__init__.py @@ -2,4 +2,4 @@ __all__ = ["Other"] -from .other import Other \ No newline at end of file +from .other import Other diff --git a/funcs/other/generators.py b/gwendolyn/funcs/other/generators.py similarity index 92% rename from funcs/other/generators.py rename to gwendolyn/funcs/other/generators.py index 37f66f9..c3a99a6 100644 --- a/funcs/other/generators.py +++ b/gwendolyn/funcs/other/generators.py @@ -17,7 +17,7 @@ class Generators(): # Generates a random name async def nameGen(self, ctx): # Makes a list of all names from "names.txt" - names = open('resources/names.txt', encoding='utf8').read() + names = open('gwendolyn/resources/names.txt', encoding='utf8').read() corpus = list(names) # Makes a list of pairs @@ -64,7 +64,7 @@ class Generators(): if random.randint(1,10) > 1: try: new_letter = random.choice(letter_dict[chain[-2]+chain[-1]]) - except: + except KeyError(): new_letter = random.choice(letter_dict[chain[-1]]) else: new_letter = random.choice(letter_dict[chain[-1]]) @@ -72,15 +72,15 @@ class Generators(): # Ends name if the name ends if new_letter == "\n": done = True - genName = "".join(chain) - self.bot.log("Generated "+genName[:-1]) + gen_name = "".join(chain) + self.bot.log("Generated "+gen_name[:-1]) # Returns the name - await ctx.send(genName) + await ctx.send(gen_name) # Generates a random tavern name async def tavernGen(self, ctx): - # Lists first parts, second parts and third parts of tavern names + # _lists first parts, second parts and third parts of tavern names fp = ["The Silver","The Golden","The Staggering","The Laughing","The Prancing","The Gilded","The Running","The Howling","The Slaughtered","The Leering","The Drunken","The Leaping","The Roaring","The Frowning","The Lonely","The Wandering","The Mysterious","The Barking","The Black","The Gleaming","The Tap-Dancing","The Sad","The Sexy","The Artificial","The Groovy","The Merciful","The Confused","The Pouting","The Horny","The Okay","The Friendly","The Hungry","The Handicapped","The Fire-breathing","The One-Eyed","The Psychotic","The Mad","The Evil","The Idiotic","The Trusty","The Busty"] sp = ["Eel","Dolphin","Dwarf","Pegasus","Pony","Rose","Stag","Wolf","Lamb","Demon","Goat","Spirit","Horde","Jester","Mountain","Eagle","Satyr","Dog","Spider","Star","Dad","Rat","Jeremy","Mouse","Unicorn","Pearl","Ant","Crab","Penguin","Octopus","Lawyer","Ghost","Toad","Handjob","Immigrant","SJW","Dragon","Bard","Sphinx","Soldier","Salmon","Owlbear","Kite","Frost Giant","Arsonist"] tp = [" Tavern"," Inn","","","","","","","","",""] diff --git a/funcs/other/nerdShit.py b/gwendolyn/funcs/other/nerd_shit.py similarity index 62% rename from funcs/other/nerdShit.py rename to gwendolyn/funcs/other/nerd_shit.py index 53f5306..22b7159 100644 --- a/funcs/other/nerdShit.py +++ b/gwendolyn/funcs/other/nerd_shit.py @@ -8,9 +8,9 @@ class NerdShit(): async def wolfSearch(self,ctx,content): await self.bot.defer(ctx) - fnt = ImageFont.truetype('resources/fonts/times-new-roman.ttf', 20) + fnt = ImageFont.truetype('gwendolyn/resources/fonts/times-new-roman.ttf', 20) self.bot.log("Requesting data") - bot = wolframalpha.Client(self.bot.credentials.wolfKey) + bot = wolframalpha.Client(self.bot.credentials["wolfram_alpha_key"]) res = bot.query(content) self.bot.log("Processing data") @@ -38,10 +38,10 @@ class NerdShit(): heights += [height] width = max(width,int(pod.img['@width'])) if titleChucks[x][count] == "": - placeForText = 0 + placeFor_text = 0 else: - placeForText = 30 - height += int(pod.img["@height"]) + 10 + placeForText + placeFor_text = 30 + height += int(pod.img["@height"]) + 10 + placeFor_text width += 10 height += 5 @@ -49,33 +49,33 @@ class NerdShit(): for count, pod in enumerate(chunk): response = requests.get(pod.img["@src"]) - file = open("resources/wolfTemp.png", "wb") + file = open("gwendolyn/resources/wolfTemp.png", "wb") file.write(response.content) file.close() - oldImage = Image.open("resources/wolfTemp.png") - oldSize = oldImage.size + old_image = Image.open("gwendolyn/resources/wolfTemp.png") + oldSize = old_image.size if titleChucks[x][count] == "": - placeForText = 0 + placeFor_text = 0 else: - placeForText = 30 - newSize = (width,int(oldSize[1]+10+placeForText)) - newImage = Image.new("RGB",newSize,color=(255,255,255)) - newImage.paste(oldImage, (int((int(oldSize[0]+10)-oldSize[0])/2),int(((newSize[1]-placeForText)-oldSize[1])/2)+placeForText)) + placeFor_text = 30 + newSize = (width,int(oldSize[1]+10+placeFor_text)) + new_image = Image.new("RGB",newSize,color=(255,255,255)) + new_image.paste(old_image, (int((int(oldSize[0]+10)-oldSize[0])/2),int(((newSize[1]-placeFor_text)-oldSize[1])/2)+placeFor_text)) if titleChucks[x][count] != "": - d = ImageDraw.Draw(newImage,"RGB") + d = ImageDraw.Draw(new_image,"RGB") d.text((5,7),titleChucks[x][count],font=fnt,fill=(150,150,150)) - wolfImage.paste(newImage,(0,heights[count])) - newImage.close() - oldImage.close() + wolfImage.paste(new_image,(0,heights[count])) + new_image.close() + old_image.close() count += 1 - wolfImage.save("resources/wolf.png") + wolfImage.save("gwendolyn/resources/wolf.png") wolfImage.close() - await ctx.channel.send(file = discord.File("resources/wolf.png")) + await ctx.channel.send(file = discord.File("gwendolyn/resources/wolf.png")) - os.remove("resources/wolf.png") - os.remove("resources/wolfTemp.png") + os.remove("gwendolyn/resources/wolf.png") + os.remove("gwendolyn/resources/wolfTemp.png") else: self.bot.log("No returned data") await ctx.send("Could not find anything relating to your search") \ No newline at end of file diff --git a/funcs/other/other.py b/gwendolyn/funcs/other/other.py similarity index 85% rename from funcs/other/other.py rename to gwendolyn/funcs/other/other.py index 2955641..f560a78 100644 --- a/funcs/other/other.py +++ b/gwendolyn/funcs/other/other.py @@ -7,11 +7,11 @@ import lxml # Used in imageFunc import fandom # Used in findWikiPage import d20 # Used in rollDice import ast -from .bedreNetflix import BedreNetflix -from .nerdShit import NerdShit +from .plex import Plex +from .nerd_shit import NerdShit from .generators import Generators -from utils import cap +from gwendolyn.utils import cap fandom.set_lang("da") fandom.set_wiki("senkulpa") @@ -19,17 +19,17 @@ fandom.set_wiki("senkulpa") class MyStringifier(d20.MarkdownStringifier): def _str_expression(self, node): if node.comment == None: - resultText = "Result" + result_text = "Result" else: - resultText = node.comment.capitalize() + result_text = node.comment.capitalize() - return f"**{resultText}**: {self._stringify(node.roll)}\n**Total**: {int(node.total)}" + return f"**{result_text}**: {self._stringify(node.roll)}\n**Total**: {int(node.total)}" class Other(): def __init__(self, bot): self.bot = bot - self.bedreNetflix = BedreNetflix(self.bot) - self.nerdShit = NerdShit(self.bot) + self.plex = Plex(self.bot) + self.nerd_shit = NerdShit(self.bot) self.generators = Generators(self.bot) # Picks a random movie and returns information about it @@ -40,12 +40,12 @@ class Other(): imdbClient = imdb.IMDb() self.bot.log("Picking a movie") - with open("resources/movies.txt", "r") as f: - movieList = f.read().split("\n") - movieName = random.choice(movieList) + with open("gwendolyn/resources/movies.txt", "r") as f: + movie_list = f.read().split("\n") + movie_name = random.choice(movie_list) - self.bot.log(f"Searching for {movieName}") - searchResult = imdbClient.search_movie(movieName) + self.bot.log(f"Searching for {movie_name}") + searchResult = imdbClient.search_movie(movie_name) self.bot.log("Getting the data") movie = searchResult[0] @@ -74,17 +74,17 @@ class Other(): author = ctx.author.display_name now = datetime.datetime.now() if time_in_range(now.replace(hour=5, minute=0, second=0, microsecond=0),now.replace(hour=10, minute=0, second=0, microsecond=0), now): - sendMessage = "Good morning, "+str(author) + send_message = "Good morning, "+str(author) elif time_in_range(now.replace(hour=13, minute=0, second=0, microsecond=0),now.replace(hour=18, minute=0, second=0, microsecond=0), now): - sendMessage = "Good afternoon, "+str(author) + send_message = "Good afternoon, "+str(author) elif time_in_range(now.replace(hour=18, minute=0, second=0, microsecond=0),now.replace(hour=22, minute=0, second=0, microsecond=0), now): - sendMessage = "Good evening, "+str(author) + send_message = "Good evening, "+str(author) elif time_in_range(now.replace(hour=22, minute=0, second=0, microsecond=0),now.replace(hour=23, minute=59, second=59, microsecond=0), now): - sendMessage = "Good night, "+str(author) + send_message = "Good night, "+str(author) else: - sendMessage = "Hello, "+str(author) + send_message = "Hello, "+str(author) - await ctx.send(sendMessage) + await ctx.send(send_message) # Finds a random picture online async def imageFunc(self, ctx): @@ -183,13 +183,13 @@ class Other(): async def helpFunc(self, ctx, command): if command == "": - with open("resources/help/help.txt",encoding="utf-8") as f: + with open("gwendolyn/resources/help/help.txt",encoding="utf-8") as f: text = f.read() em = discord.Embed(title = "Help", description = text,colour = 0x59f442) await ctx.send(embed = em) else: self.bot.log(f"Looking for help-{command}.txt",str(ctx.channel_id)) - with open(f"resources/help/help-{command}.txt",encoding="utf-8") as f: + with open(f"gwendolyn/resources/help/help-{command}.txt",encoding="utf-8") as f: text = f.read() em = discord.Embed(title = command.capitalize(), description = text,colour = 0x59f442) await ctx.send(embed = em) diff --git a/gwendolyn/funcs/other/plex.py b/gwendolyn/funcs/other/plex.py new file mode 100644 index 0000000..953911d --- /dev/null +++ b/gwendolyn/funcs/other/plex.py @@ -0,0 +1,536 @@ +"""Plex integration with the bot.""" +from math import floor, ceil +import time +import json +import asyncio +import requests +import imdb +import discord + +class Plex(): + """Container for Plex functions and commands.""" + def __init__(self,bot): + self.bot = bot + self.credentials = self.bot.credentials + self.long_strings = self.bot.long_strings + server_ip = ["localhost", "192.168.0.40"][self.bot.options["testing"]] + + self.radarr_url = "http://"+server_ip+":7878/api/v3/" + self.sonarr_url = "http://"+server_ip+":8989/api/" + self.qbittorrent_url = "http://"+server_ip+":8080/api/v2/" + self.movie_path = "/media/plex/Server/movies/" + self.show_path = "/media/plex/Server/Shows/" + + + async def request_movie(self, ctx, movie_name): + """Request a movie for the Plex Server""" + await self.bot.defer(ctx) + + self.bot.log("Searching for "+movie_name) + movie_list = imdb.IMDb().search_movie(movie_name) + movies = [] + for movie in movie_list: + if movie["kind"] == "movie": + movies.append(movie) + if len(movies) > 5: + movies = movies[:5] + + if len(movies) == 1: + message_title = "**Is it this movie?**" + else: + message_title = "**Is it any of these movies?**" + + message_text = "" + imdb_ids = [] + + for i, movie in enumerate(movies): + try: + message_text += "\n"+str(i+1)+") "+movie["title"] + try: + message_text += " ("+str(movie["year"])+")" + except KeyError: + self.bot.log(f"{movie['title']} has no year.") + except KeyError: + message_text += "Error" + imdb_ids.append(movie.movieID) + + self.bot.log( + f"Returning a list of {len(movies)} possible movies: {imdb_ids}" + ) + + embed = discord.Embed( + title=message_title, + description=message_text, + 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) + + if len(movies) == 1: + await message.add_reaction("✔️") + else: + for i in range(len(movies)): + 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 != "" 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: + 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") + else: + self.bot.log("Trying to add movie "+str(imdb_id)) + 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) + lookup_data = response.json() + post_data = {"qualityProfileId": 1, + "rootFolder_path" : self.movie_path, + "monitored" : True, + "addOptions": {"searchForMovie": True}} + for key in ["tmdbId","title","titleSlug","images","year"]: + post_data.update({key : lookup_data[key]}) + + response = requests.post( + url= self.radarr_url+"movie?apikey="+api_key, + json = post_data + ) + + if response.status_code == 201: + success_message = "{} 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( + 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) + + async def request_show(self, ctx, show_name): + """Request a show for the Plex server.""" + await self.bot.defer(ctx) + + self.bot.log("Searching for "+show_name) + movies = imdb.IMDb().search_movie(show_name) # Replace with tvdb + shows = [] + for movie in movies: + if movie["kind"] in ["tv series","tv miniseries"]: + shows.append(movie) + if len(shows) > 5: + shows = shows[:5] + + if len(shows) == 1: + message_title = "**Is it this show?**" + else: + message_title = "**Is it any of these shows?**" + + message_text = "" + imdb_names = [] + + for i, show in enumerate(shows): + try: + message_text += f"\n{i+1}) {show['title']} ({show['year']})" + except KeyError: + try: + message_text += "\n"+str(i+1)+") "+show["title"] + except KeyError: + message_text += "Error" + imdb_names.append(show["title"]) + + self.bot.log( + f"Returning a list of {len(shows)} possible shows: {imdb_names}" + ) + + embed = discord.Embed( + title=message_title, + description=message_text, + 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) + + 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" + ) + else: + self.bot.log("Trying to add show "+str(imdb_name)) + api_key = self.credentials["sonarr_key"] + request_url = self.sonarr_url+"series/lookup?term=" + request_url += imdb_name.replace(" ","%20") + request_url += "&apiKey="+api_key + response = requests.get(request_url) + lookup_data = response.json()[0] + post_data = { + "ProfileId" : 1, + "rootFolder_path" : self.show_path, + "monitored" : True, + "addOptions" : {"searchForMissingEpisodes" : True} + } + for key in ["tvdbId","title","titleSlug","images","seasons"]: + post_data.update({key : lookup_data[key]}) + + + response = requests.post( + url= self.sonarr_url+"series?apikey="+api_key, + json = post_data + ) + + 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") + elif response.status_code == 400: + 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) + + async def __generate_download_list(self, show_dm, show_movies, show_shows, + episodes): + """Generate a list of all torrents. + + *Returns* + message_text: str + A formatted list of all torrents + + all_downloaded: bool + Whether all torrents are downloaded + """ + self.bot.log("Generating torrent list") + title_width = 100 + message = [] + all_downloaded = True + + if show_dm: + message.append("") + dm_section_title = "*Torrent Downloads*" + dm_section_title_line = "-"*((title_width-len(dm_section_title))//2) + message.append( + dm_section_title_line+dm_section_title+dm_section_title_line + ) + response = requests.get(self.qbittorrent_url+"torrents/info") + torrent_list = response.json() + + if len(torrent_list) > 0: + for torrent in torrent_list: + torrent_name = torrent["name"] + if len(torrent_name) > 30: + if torrent_name[26] == " ": + torrent_name = torrent_name[:26]+"...." + else: + torrent_name = torrent_name[:27]+"..." + while len(torrent_name) < 30: + torrent_name += " " + + if torrent["size"] == 0: + download_ratio = 0 + elif torrent["amount_left"] == 0: + download_ratio = 1 + else: + download_ratio = min( + torrent["downloaded"]/torrent["size"], + 1 + ) + progress_bar = "|"+("█"*floor(download_ratio*20)) + while len(progress_bar) < 21: + progress_bar += " " + + progress_bar += "| "+str(floor(download_ratio*100))+"%" + + while len(progress_bar) < 27: + progress_bar += " " + + eta_in_seconds = torrent["eta"] + + if eta_in_seconds >= 8640000: + eta = "∞" + else: + eta = "" + if eta_in_seconds >= 86400: + eta += str(floor(eta_in_seconds/86400))+"d " + if eta_in_seconds >= 3600: + eta += str(floor((eta_in_seconds%86400)/3600))+"h " + if eta_in_seconds >= 60: + eta += str(floor((eta_in_seconds%3600)/60))+"m " + + eta += str(eta_in_seconds%60)+"s" + + torrent_info = f"{torrent_name} {progress_bar} " + torrent_info += f"(Eta: {eta})" + + if torrent["state"] == "stalledDL": + torrent_info += " (Stalled)" + + if not (download_ratio == 1 and + torrent["last_activity"] < time.time()-7200): + message.append(torrent_info) + + if download_ratio < 1 and torrent["state"] != "stalledDL": + all_downloaded = False + else: + message.append("No torrents currently downloading") + + if show_movies: + message.append("") + movies_section_title = "*Missing movies not downloading*" + movies_section_line = ( + "-"*((title_width-len(movies_section_title))//2) + ) + message.append( + movies_section_line+movies_section_title+movies_section_line + ) + movie_list = requests.get( + self.radarr_url+"movie?api_key="+self.credentials["radarr_key"] + ).json() + movie_queue = requests.get( + self.radarr_url+"queue?api_key="+self.credentials["radarr_key"] + ).json() + movie_queue_ids = [] + + for queue_item in movie_queue["records"]: + movie_queue_ids.append(queue_item["movieId"]) + + for movie in movie_list: + if (not movie["hasFile"] and + movie["id"] not in movie_queue_ids): + movie_name = movie["title"] + if len(movie_name) > 40: + if movie_name[36] == " ": + movie_name = movie_name[:36]+"...." + else: + movie_name = movie_name[:37]+"..." + + while len(movie_name) < 41: + movie_name += " " + + if movie["monitored"]: + movie_info = movie_name+"Could not find a torrent" + else: + movie_info = self.long_strings["No torrent"].format( + movie_name + ) + + message.append(movie_info) + + if show_shows: + message.append("") + show_section_title = "*Missing shows not downloading*" + show_section_line = "-"*((title_width-len(show_section_title))//2) + message.append( + show_section_line+show_section_title+show_section_line + ) + + show_list = requests.get( + self.sonarr_url+"series?api_key="+self.credentials["sonarr_key"] + ).json() + + for show in show_list: + if show["seasons"][0]["seasonNumber"] == 0: + seasons = show["seasons"][1:] + else: + seasons = show["seasons"] + if any( + ( + i["statistics"]["episodeCount"] != + i["statistics"]["totalEpisodeCount"] + ) for i in seasons): + if all( + i["statistics"]["episodeCount"] == 0 for i in seasons + ): + message.append(show["title"] + " (all episodes)") + else: + if episodes: + missing_episodes = sum( + (i["statistics"]["totalEpisodeCount"] - + i["statistics"]["episodeCount"]) + for i in seasons) + message.append( + f"{show['title']} ({missing_episodes} episodes)" + ) + + message.append("-"*title_width) + + message_text = "```"+"\n".join(message[1:])+"```" + if message_text == "``````": + message_text = self.long_strings["No torrents downloading"] + return message_text, all_downloaded + + async def downloading(self, ctx, content): + """Send message with list of all downloading torrents.""" + async def send_long_message(ctx,message_text): + if len(message_text) <= 1994: + await ctx.send("```"+message_text+"```") + else: + cut_off_index = message_text[:1994].rfind("\n") + await ctx.send("```"+message_text[:cut_off_index]+"```") + await send_long_message(ctx,message_text[cut_off_index+1:]) + + await self.bot.defer(ctx) + + # showDM, showMovies, showShows, episodes + parameters = [False, False, False, False] + show_dm_args = ["d", "dm", "downloading", "downloadmanager"] + show_movies_args = ["m", "movies"] + show_shows_args = ["s", "shows", "series"] + show_episode_args = ["e", "episodes"] + arg_list = [ + show_dm_args, show_movies_args, show_shows_args, show_episode_args + ] + input_args = [] + valid_arguments = True + + while content != "" and valid_arguments: + if content[0] == " ": + content = content[1:] + elif content[0] == "-": + if content[1] == "-": + arg_start = 2 + if " " in content: + arg_stop = content.find(" ") + else: + arg_stop = None + else: + arg_start = 1 + arg_stop = 2 + + input_args.append(content[arg_start:arg_stop]) + if arg_stop is None: + content = "" + else: + content = content[arg_stop:] + else: + valid_arguments = False + + if valid_arguments: + for arg_index, arg_aliases in enumerate(arg_list): + arg_in_input = [i in input_args for i in arg_aliases] + if any(arg_in_input): + input_args.remove(arg_aliases[arg_in_input.index(True)]) + parameters[arg_index] = True + + if len(input_args) != 0 or (not parameters[2] and parameters[3]): + valid_arguments = False + + show_anything = any(i for i in parameters) + if not (valid_arguments and show_anything): + await ctx.send(self.long_strings["Invalid parameters"]) + else: + message_text, all_downloaded = await self.__generate_download_list( + *parameters + ) + if not message_text.startswith("```"): + await ctx.send(message_text) + + elif len(message_text) > 2000: + message_text = message_text[3:-3] + await send_long_message(ctx,message_text) + + elif all_downloaded: + await ctx.send(message_text) + + else: + updates_left = 60 + message_text = self.long_strings["Update"].format( + message_text[:-3], ceil(updates_left/6) + ) + old_message = await ctx.send(message_text) + + while ((not all_downloaded) and updates_left > 0): + await asyncio.sleep(10) + updates_left -= 1 + message_text, all_downloaded = await ( + self.__generate_download_list(*parameters) + ) + message_text = self.long_strings["Update"].format( + message_text[:-3], + ceil(updates_left/6) + ) + await old_message.edit(content = message_text) + + message_text, all_downloaded = await ( + self.__generate_download_list(*parameters) + ) + + if message_text.startswith("```"): + if all_downloaded: + self.bot.log("All torrents are downloaded") + else: + message_text = self.long_strings["No updates"].format( + message_text[:-3] + ) + self.bot.log("The message updated 20 times") + + await old_message.edit(content = message_text) diff --git a/funcs/starWarsFuncs/__init__.py b/gwendolyn/funcs/star_wars_funcs/__init__.py similarity index 70% rename from funcs/starWarsFuncs/__init__.py rename to gwendolyn/funcs/star_wars_funcs/__init__.py index 041ebad..8089a50 100644 --- a/funcs/starWarsFuncs/__init__.py +++ b/gwendolyn/funcs/star_wars_funcs/__init__.py @@ -2,4 +2,4 @@ __all__ = ["StarWars"] -from .starWars import StarWars \ No newline at end of file +from .star_wars import StarWars \ No newline at end of file diff --git a/funcs/starWarsFuncs/starWars.py b/gwendolyn/funcs/star_wars_funcs/star_wars.py similarity index 62% rename from funcs/starWarsFuncs/starWars.py rename to gwendolyn/funcs/star_wars_funcs/star_wars.py index 48e6f84..43fb417 100644 --- a/funcs/starWarsFuncs/starWars.py +++ b/gwendolyn/funcs/star_wars_funcs/star_wars.py @@ -1,6 +1,6 @@ -from .starWarsChar import StarWarsChar -from .starWarsRoll import StarWarsRoll -from .starWarsDestiny import StarWarsDestiny +from .star_wars_char import StarWarsChar +from .star_wars_roll import StarWarsRoll +from .star_wars_destiny import StarWarsDestiny class StarWars(): def __init__(self, bot): diff --git a/funcs/starWarsFuncs/starWarsChar.py b/gwendolyn/funcs/star_wars_funcs/star_wars_char.py similarity index 96% rename from funcs/starWarsFuncs/starWarsChar.py rename to gwendolyn/funcs/star_wars_funcs/star_wars_char.py index 30cb2f3..ebfa53a 100644 --- a/funcs/starWarsFuncs/starWarsChar.py +++ b/gwendolyn/funcs/star_wars_funcs/star_wars_char.py @@ -6,16 +6,16 @@ class StarWarsChar(): def __init__(self, bot): self.bot = bot - def getCharName(self, user : str): - self.bot.log("Getting name for "+self.bot.databaseFuncs.getName(user)+"'s character") + def getChar_name(self, user : str): + self.bot.log("Getting name for "+self.bot.database_funcs.get_name(user)+"'s character") userCharacter = self.bot.database["starwars characters"].find_one({"_id":user}) if userCharacter != None: self.bot.log("Name is "+userCharacter["Name"]) return userCharacter["Name"] else: - self.bot.log("Just using "+self.bot.databaseFuncs.getName(user)) - return self.bot.databaseFuncs.getName(user) + self.bot.log("Just using "+self.bot.database_funcs.get_name(user)) + return self.bot.database_funcs.get_name(user) def setUpDict(self, cmd : dict): self.bot.log("Setting up a dictionary in a nice way") @@ -239,7 +239,7 @@ class StarWarsChar(): return name, text1+text2+"\n\n"+text3+divider+text4+"\n"+divider+text5+text6+text7+text8 - def charData(self,user : str,cmd : str): + def char_data(self,user : str,cmd : str): userCharacter = self.bot.database["starwars characters"].find_one({"_id":user}) key = string.capwords(cmd.split(" ")[0]) @@ -252,7 +252,7 @@ class StarWarsChar(): if cmd == "": break - self.bot.log("Looking for "+self.bot.databaseFuncs.getName(user)+"'s character") + self.bot.log("Looking for "+self.bot.database_funcs.get_name(user)+"'s character") if userCharacter != None: self.bot.log("Found it! Looking for "+key+" in the data") if key in userCharacter: @@ -303,7 +303,7 @@ class StarWarsChar(): return cmd[0]+" added to "+key+" for " + userCharacter["Name"] elif key == "Weapons": - with open("resources/starWars/starwarstemplates.json", "r") as f: + with open("gwendolyn/resources/star_wars/starwarstemplates.json", "r") as f: templates = json.load(f) newWeapon = templates["Weapon"] self.bot.log("Adding "+cmd+" to "+key) @@ -495,20 +495,20 @@ class StarWarsChar(): text = self.replaceWithSpaces(text) returnEmbed = True else: - self.bot.log("Makin' a character for "+self.bot.databaseFuncs.getName(user)) - with open("resources/starWars/starwarstemplates.json", "r") as f: + self.bot.log("Makin' a character for "+self.bot.database_funcs.get_name(user)) + with open("gwendolyn/resources/star_wars/starwarstemplates.json", "r") as f: templates = json.load(f) newChar = templates["Character"] newChar["_id"] = user self.bot.database["starwars characters"].insert_one(newChar) - await ctx.send("Character for " + self.bot.databaseFuncs.getName(user) + " created") + await ctx.send("Character for " + self.bot.database_funcs.get_name(user) + " created") else: if cmd == "Purge": - self.bot.log("Deleting "+self.bot.databaseFuncs.getName(user)+"'s character") + self.bot.log("Deleting "+self.bot.database_funcs.get_name(user)+"'s character") self.bot.database["starwars characters"].delete_one({"_id":user}) - await ctx.send("Character for " + self.bot.databaseFuncs.getName(user) + " deleted") + await ctx.send("Character for " + self.bot.database_funcs.get_name(user) + " deleted") else: - await ctx.send(self.replaceWithSpaces(str(self.charData(user,cmd)))) + await ctx.send(self.replaceWithSpaces(str(self.char_data(user,cmd)))) if returnEmbed: em = discord.Embed(title = title, description = text, colour=0xDEADBF) diff --git a/funcs/starWarsFuncs/starWarsDestiny.py b/gwendolyn/funcs/star_wars_funcs/star_wars_destiny.py similarity index 60% rename from funcs/starWarsFuncs/starWarsDestiny.py rename to gwendolyn/funcs/star_wars_funcs/star_wars_destiny.py index 527d31a..1f5a7c2 100644 --- a/funcs/starWarsFuncs/starWarsDestiny.py +++ b/gwendolyn/funcs/star_wars_funcs/star_wars_destiny.py @@ -4,16 +4,16 @@ class StarWarsDestiny(): def destinyNew(self, num : int): self.bot.log("Creating a new destiny pool with "+str(num)+" players") - roll, diceResults = self.bot.starWars.roll.roll(0,0,0,0,0,0,num) + roll, diceResults = self.bot.star_wars.roll.roll(0,0,0,0,0,0,num) roll = "".join(sorted(roll)) - with open("resources/starWars/destinyPoints.txt","wt") as f: + with open("gwendolyn/resources/star_wars/destinyPoints.txt","wt") as f: f.write(roll) - return "Rolled for Destiny Points and got:\n"+self.bot.starWars.roll.diceResultToEmoji(diceResults)+"\n"+self.bot.starWars.roll.resultToEmoji(roll) + return "Rolled for Destiny Points and got:\n"+self.bot.star_wars.roll.diceResultToEmoji(diceResults)+"\n"+self.bot.star_wars.roll.resultToEmoji(roll) def destinyUse(self, user : str): - with open("resources/starWars/destinyPoints.txt","rt") as f: + with open("gwendolyn/resources/star_wars/destinyPoints.txt","rt") as f: points = f.read() if user == "Nikolaj": @@ -21,10 +21,10 @@ class StarWarsDestiny(): if 'B' in points: points = points.replace("B","L",1) points = "".join(sorted(points)) - with open("resources/starWars/destinyPoints.txt","wt") as f: + with open("gwendolyn/resources/star_wars/destinyPoints.txt","wt") as f: f.write(points) self.bot.log("Did it") - return "Used a dark side destiny point. Destiny pool is now:\n"+self.bot.starWars.roll.resultToEmoji(points) + return "Used a dark side destiny point. Destiny pool is now:\n"+self.bot.star_wars.roll.resultToEmoji(points) else: self.bot.log("There were no dark side destiny points") return "No dark side destiny points" @@ -33,10 +33,10 @@ class StarWarsDestiny(): if 'L' in points: points = points.replace("L","B",1) points = "".join(sorted(points)) - with open("resources/starWars/destinyPoints.txt","wt") as f: + with open("gwendolyn/resources/star_wars/destinyPoints.txt","wt") as f: f.write(points) self.bot.log("Did it") - return "Used a light side destiny point. Destiny pool is now:\n"+self.bot.starWars.roll.resultToEmoji(points) + return "Used a light side destiny point. Destiny pool is now:\n"+self.bot.star_wars.roll.resultToEmoji(points) else: self.bot.log("There were no dark side destiny points") return "No light side destiny points" @@ -51,22 +51,22 @@ class StarWarsDestiny(): if cmd == "": self.bot.log("Retrieving destiny pool info") - with open("resources/starWars/destinyPoints.txt","rt") as f: - sendMessage = self.bot.starWars.roll.resultToEmoji(f.read()) + with open("gwendolyn/resources/star_wars/destinyPoints.txt","rt") as f: + send_message = self.bot.star_wars.roll.resultToEmoji(f.read()) else: commands = cmd.upper().split(" ") if commands[0] == "N": if len(commands) > 1: - sendMessage = self.destinyNew(int(commands[1])) + send_message = self.destinyNew(int(commands[1])) else: - sendMessage = "You need to give an amount of players (error code 921)" + send_message = "You need to give an amount of players (error code 921)" elif commands[0] == "U": - sendMessage = self.destinyUse(user) + send_message = self.destinyUse(user) else: - sendMessage = "I didn't quite understand that (error code 922)" + send_message = "I didn't quite understand that (error code 922)" - messageList = sendMessage.split("\n") - await ctx.send(messageList[0]) - if len(messageList) > 1: - for messageItem in messageList[1:]: + message_list = send_message.split("\n") + await ctx.send(message_list[0]) + if len(message_list) > 1: + for messageItem in message_list[1:]: await ctx.channel.send(messageItem) diff --git a/funcs/starWarsFuncs/starWarsRoll.py b/gwendolyn/funcs/star_wars_funcs/star_wars_roll.py similarity index 90% rename from funcs/starWarsFuncs/starWarsRoll.py rename to gwendolyn/funcs/star_wars_funcs/star_wars_roll.py index e7174ad..c923a5c 100644 --- a/funcs/starWarsFuncs/starWarsRoll.py +++ b/gwendolyn/funcs/star_wars_funcs/star_wars_roll.py @@ -3,8 +3,8 @@ import re import string import json -with open("resources/starWars/starwarsskills.json", "r") as f: - skillData = json.load(f) +with open("gwendolyn/resources/star_wars/starwarsskills.json", "r") as f: + skill_data = json.load(f) class StarWarsRoll(): def __init__(self, bot): @@ -288,12 +288,12 @@ class StarWarsRoll(): characteristic = random.choice(["brawn"] * 3 + ["agility"] * 3 + ["intellect", "cunning", "presence"]) results = "**Gruesome Injury**: The target's "+characteristic+" is permanently one lower, "+dd+dd+dd+dd - sendMessage = "Roll: "+str(roll)+"\nInjury:\n"+results + send_message = "Roll: "+str(roll)+"\nInjury:\n"+results - messageList = sendMessage.split("\n") - await ctx.send(messageList[0]) - if len(messageList) > 1: - for messageItem in messageList[1:]: + message_list = send_message.split("\n") + await ctx.send(message_list[0]) + if len(message_list) > 1: + for messageItem in message_list[1:]: await ctx.channel.send(messageItem) # Parses the command into something the other functions understand @@ -302,7 +302,7 @@ class StarWarsRoll(): cmd = re.sub(' +',' ',cmd.upper()) + " " if cmd[0] == " ": cmd = cmd[1:] - cmd = self.bot.starWars.character.replaceSpaces(string.capwords(cmd)) + cmd = self.bot.star_wars.character.replaceSpaces(string.capwords(cmd)) commands = cmd.split(" ") validCommand = False @@ -312,19 +312,19 @@ class StarWarsRoll(): rollParameters = [0,0,0,0,0,0,0] if string.capwords(commands[0]) == "Obligations": - sendMessage = self.obligationRoll() + send_message = self.obligationRoll() - elif string.capwords(commands[0]) in skillData: + elif string.capwords(commands[0]) in skill_data: self.bot.log("Oh look! This guy has skills!") - if self.bot.starWars.character.userHasChar(user): + if self.bot.star_wars.character.userHasChar(user): self.bot.log("They have a character. That much we know") - skillLevel = self.bot.starWars.character.charData(user,"Skills " + string.capwords(commands[0])) + skillLevel = self.bot.star_wars.character.char_data(user,"Skills " + string.capwords(commands[0])) if string.capwords(commands[0]) == "Lightsaber": self.bot.log("The skill is lightsaber") - charLevel = self.bot.starWars.character.charData(user,"Characteristics " + self.bot.starWars.character.lightsaberChar(user)) + charLevel = self.bot.star_wars.character.char_data(user,"Characteristics " + self.bot.star_wars.character.lightsaberChar(user)) else: - charLevel = self.bot.starWars.character.charData(user,"Characteristics " + skillData[string.capwords(commands[0])]) + charLevel = self.bot.star_wars.character.char_data(user,"Characteristics " + skill_data[string.capwords(commands[0])]) abilityDice = abs(charLevel-skillLevel) proficiencyDice = min(skillLevel,charLevel) @@ -334,14 +334,14 @@ class StarWarsRoll(): validCommand = True else: self.bot.log("Okay, no they don't i guess") - sendMessage = "You don't have a user. You can make one with /starwarscharacter" + send_message = "You don't have a user. You can make one with /starwarscharacter" elif string.capwords(commands[0]) in ["Ranged","Piloting"]: self.bot.log("They fucked up writing the name of a ranged or piloting skill") if string.capwords(commands[0]) == "Ranged": - sendMessage = "Did you mean \"Ranged - Heavy\" or \"Ranged - Light\" (error code 913)" + send_message = "Did you mean \"Ranged - Heavy\" or \"Ranged - Light\" (error code 913)" else: - sendMessage = "Did you mean \"Piloting - Planetary\" or \"Piloting - Space\" (error code 913)" + send_message = "Did you mean \"Piloting - Planetary\" or \"Piloting - Space\" (error code 913)" else: validCommand = True @@ -372,19 +372,19 @@ class StarWarsRoll(): simplified = self.simplify(rollResults) - name = self.bot.starWars.character.getCharName(user) + name = self.bot.star_wars.character.getChar_name(user) self.bot.log("Returns results and simplified results") if simplified == "": - sendMessage = name + " rolls: " + "\n" + self.diceResultToEmoji(diceResults) + "\nEverything cancels out!" + send_message = name + " rolls: " + "\n" + self.diceResultToEmoji(diceResults) + "\nEverything cancels out!" else: - sendMessage = name + " rolls: " + "\n" + self.diceResultToEmoji(diceResults) + "\n" + self.resultToEmoji(simplified) + send_message = name + " rolls: " + "\n" + self.diceResultToEmoji(diceResults) + "\n" + self.resultToEmoji(simplified) - messageList = sendMessage.split("\n") - await ctx.send(messageList[0]) - if len(messageList) > 1: - for messageItem in messageList[1:]: + message_list = send_message.split("\n") + await ctx.send(message_list[0]) + if len(message_list) > 1: + for messageItem in message_list[1:]: if messageItem == "": self.bot.log("Tried to send empty message") else: diff --git a/gwendolyn/gwendolyn_client.py b/gwendolyn/gwendolyn_client.py new file mode 100644 index 0000000..ff4c683 --- /dev/null +++ b/gwendolyn/gwendolyn_client.py @@ -0,0 +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") diff --git a/resources/fonts/comic-sans-bold.ttf b/gwendolyn/resources/fonts/comic-sans-bold.ttf similarity index 100% rename from resources/fonts/comic-sans-bold.ttf rename to gwendolyn/resources/fonts/comic-sans-bold.ttf diff --git a/resources/fonts/futura-bold.ttf b/gwendolyn/resources/fonts/futura-bold.ttf similarity index 100% rename from resources/fonts/futura-bold.ttf rename to gwendolyn/resources/fonts/futura-bold.ttf diff --git a/resources/fonts/times-new-roman.ttf b/gwendolyn/resources/fonts/times-new-roman.ttf similarity index 100% rename from resources/fonts/times-new-roman.ttf rename to gwendolyn/resources/fonts/times-new-roman.ttf diff --git a/resources/games/blackjackTable.png b/gwendolyn/resources/games/blackjack_table.png similarity index 100% rename from resources/games/blackjackTable.png rename to gwendolyn/resources/games/blackjack_table.png diff --git a/resources/games/cards/0C.png b/gwendolyn/resources/games/cards/0C.png similarity index 100% rename from resources/games/cards/0C.png rename to gwendolyn/resources/games/cards/0C.png diff --git a/resources/games/cards/0D.png b/gwendolyn/resources/games/cards/0D.png similarity index 100% rename from resources/games/cards/0D.png rename to gwendolyn/resources/games/cards/0D.png diff --git a/resources/games/cards/0H.png b/gwendolyn/resources/games/cards/0H.png similarity index 100% rename from resources/games/cards/0H.png rename to gwendolyn/resources/games/cards/0H.png diff --git a/resources/games/cards/0S.png b/gwendolyn/resources/games/cards/0S.png similarity index 100% rename from resources/games/cards/0S.png rename to gwendolyn/resources/games/cards/0S.png diff --git a/resources/games/cards/2C.png b/gwendolyn/resources/games/cards/2C.png similarity index 100% rename from resources/games/cards/2C.png rename to gwendolyn/resources/games/cards/2C.png diff --git a/resources/games/cards/2D.png b/gwendolyn/resources/games/cards/2D.png similarity index 100% rename from resources/games/cards/2D.png rename to gwendolyn/resources/games/cards/2D.png diff --git a/resources/games/cards/2H.png b/gwendolyn/resources/games/cards/2H.png similarity index 100% rename from resources/games/cards/2H.png rename to gwendolyn/resources/games/cards/2H.png diff --git a/resources/games/cards/2S.png b/gwendolyn/resources/games/cards/2S.png similarity index 100% rename from resources/games/cards/2S.png rename to gwendolyn/resources/games/cards/2S.png diff --git a/resources/games/cards/3C.png b/gwendolyn/resources/games/cards/3C.png similarity index 100% rename from resources/games/cards/3C.png rename to gwendolyn/resources/games/cards/3C.png diff --git a/resources/games/cards/3D.png b/gwendolyn/resources/games/cards/3D.png similarity index 100% rename from resources/games/cards/3D.png rename to gwendolyn/resources/games/cards/3D.png diff --git a/resources/games/cards/3H.png b/gwendolyn/resources/games/cards/3H.png similarity index 100% rename from resources/games/cards/3H.png rename to gwendolyn/resources/games/cards/3H.png diff --git a/resources/games/cards/3S.png b/gwendolyn/resources/games/cards/3S.png similarity index 100% rename from resources/games/cards/3S.png rename to gwendolyn/resources/games/cards/3S.png diff --git a/resources/games/cards/4C.png b/gwendolyn/resources/games/cards/4C.png similarity index 100% rename from resources/games/cards/4C.png rename to gwendolyn/resources/games/cards/4C.png diff --git a/resources/games/cards/4D.png b/gwendolyn/resources/games/cards/4D.png similarity index 100% rename from resources/games/cards/4D.png rename to gwendolyn/resources/games/cards/4D.png diff --git a/resources/games/cards/4H.png b/gwendolyn/resources/games/cards/4H.png similarity index 100% rename from resources/games/cards/4H.png rename to gwendolyn/resources/games/cards/4H.png diff --git a/resources/games/cards/4S.png b/gwendolyn/resources/games/cards/4S.png similarity index 100% rename from resources/games/cards/4S.png rename to gwendolyn/resources/games/cards/4S.png diff --git a/resources/games/cards/5C.png b/gwendolyn/resources/games/cards/5C.png similarity index 100% rename from resources/games/cards/5C.png rename to gwendolyn/resources/games/cards/5C.png diff --git a/resources/games/cards/5D.png b/gwendolyn/resources/games/cards/5D.png similarity index 100% rename from resources/games/cards/5D.png rename to gwendolyn/resources/games/cards/5D.png diff --git a/resources/games/cards/5H.png b/gwendolyn/resources/games/cards/5H.png similarity index 100% rename from resources/games/cards/5H.png rename to gwendolyn/resources/games/cards/5H.png diff --git a/resources/games/cards/5S.png b/gwendolyn/resources/games/cards/5S.png similarity index 100% rename from resources/games/cards/5S.png rename to gwendolyn/resources/games/cards/5S.png diff --git a/resources/games/cards/6C.png b/gwendolyn/resources/games/cards/6C.png similarity index 100% rename from resources/games/cards/6C.png rename to gwendolyn/resources/games/cards/6C.png diff --git a/resources/games/cards/6D.png b/gwendolyn/resources/games/cards/6D.png similarity index 100% rename from resources/games/cards/6D.png rename to gwendolyn/resources/games/cards/6D.png diff --git a/resources/games/cards/6H.png b/gwendolyn/resources/games/cards/6H.png similarity index 100% rename from resources/games/cards/6H.png rename to gwendolyn/resources/games/cards/6H.png diff --git a/resources/games/cards/6S.png b/gwendolyn/resources/games/cards/6S.png similarity index 100% rename from resources/games/cards/6S.png rename to gwendolyn/resources/games/cards/6S.png diff --git a/resources/games/cards/7C.png b/gwendolyn/resources/games/cards/7C.png similarity index 100% rename from resources/games/cards/7C.png rename to gwendolyn/resources/games/cards/7C.png diff --git a/resources/games/cards/7D.png b/gwendolyn/resources/games/cards/7D.png similarity index 100% rename from resources/games/cards/7D.png rename to gwendolyn/resources/games/cards/7D.png diff --git a/resources/games/cards/7H.png b/gwendolyn/resources/games/cards/7H.png similarity index 100% rename from resources/games/cards/7H.png rename to gwendolyn/resources/games/cards/7H.png diff --git a/resources/games/cards/7S.png b/gwendolyn/resources/games/cards/7S.png similarity index 100% rename from resources/games/cards/7S.png rename to gwendolyn/resources/games/cards/7S.png diff --git a/resources/games/cards/8C.png b/gwendolyn/resources/games/cards/8C.png similarity index 100% rename from resources/games/cards/8C.png rename to gwendolyn/resources/games/cards/8C.png diff --git a/resources/games/cards/8D.png b/gwendolyn/resources/games/cards/8D.png similarity index 100% rename from resources/games/cards/8D.png rename to gwendolyn/resources/games/cards/8D.png diff --git a/resources/games/cards/8H.png b/gwendolyn/resources/games/cards/8H.png similarity index 100% rename from resources/games/cards/8H.png rename to gwendolyn/resources/games/cards/8H.png diff --git a/resources/games/cards/8S.png b/gwendolyn/resources/games/cards/8S.png similarity index 100% rename from resources/games/cards/8S.png rename to gwendolyn/resources/games/cards/8S.png diff --git a/resources/games/cards/9C.png b/gwendolyn/resources/games/cards/9C.png similarity index 100% rename from resources/games/cards/9C.png rename to gwendolyn/resources/games/cards/9C.png diff --git a/resources/games/cards/9D.png b/gwendolyn/resources/games/cards/9D.png similarity index 100% rename from resources/games/cards/9D.png rename to gwendolyn/resources/games/cards/9D.png diff --git a/resources/games/cards/9H.png b/gwendolyn/resources/games/cards/9H.png similarity index 100% rename from resources/games/cards/9H.png rename to gwendolyn/resources/games/cards/9H.png diff --git a/resources/games/cards/9S.png b/gwendolyn/resources/games/cards/9S.png similarity index 100% rename from resources/games/cards/9S.png rename to gwendolyn/resources/games/cards/9S.png diff --git a/resources/games/cards/AC.png b/gwendolyn/resources/games/cards/AC.png similarity index 100% rename from resources/games/cards/AC.png rename to gwendolyn/resources/games/cards/AC.png diff --git a/resources/games/cards/AD.png b/gwendolyn/resources/games/cards/AD.png similarity index 100% rename from resources/games/cards/AD.png rename to gwendolyn/resources/games/cards/AD.png diff --git a/resources/games/cards/AH.png b/gwendolyn/resources/games/cards/AH.png similarity index 100% rename from resources/games/cards/AH.png rename to gwendolyn/resources/games/cards/AH.png diff --git a/resources/games/cards/AS.png b/gwendolyn/resources/games/cards/AS.png similarity index 100% rename from resources/games/cards/AS.png rename to gwendolyn/resources/games/cards/AS.png diff --git a/resources/games/cards/JC.png b/gwendolyn/resources/games/cards/JC.png similarity index 100% rename from resources/games/cards/JC.png rename to gwendolyn/resources/games/cards/JC.png diff --git a/resources/games/cards/JD.png b/gwendolyn/resources/games/cards/JD.png similarity index 100% rename from resources/games/cards/JD.png rename to gwendolyn/resources/games/cards/JD.png diff --git a/resources/games/cards/JH.png b/gwendolyn/resources/games/cards/JH.png similarity index 100% rename from resources/games/cards/JH.png rename to gwendolyn/resources/games/cards/JH.png diff --git a/resources/games/cards/JS.png b/gwendolyn/resources/games/cards/JS.png similarity index 100% rename from resources/games/cards/JS.png rename to gwendolyn/resources/games/cards/JS.png diff --git a/resources/games/cards/KC.png b/gwendolyn/resources/games/cards/KC.png similarity index 100% rename from resources/games/cards/KC.png rename to gwendolyn/resources/games/cards/KC.png diff --git a/resources/games/cards/KD.png b/gwendolyn/resources/games/cards/KD.png similarity index 100% rename from resources/games/cards/KD.png rename to gwendolyn/resources/games/cards/KD.png diff --git a/resources/games/cards/KH.png b/gwendolyn/resources/games/cards/KH.png similarity index 100% rename from resources/games/cards/KH.png rename to gwendolyn/resources/games/cards/KH.png diff --git a/resources/games/cards/KS.png b/gwendolyn/resources/games/cards/KS.png similarity index 100% rename from resources/games/cards/KS.png rename to gwendolyn/resources/games/cards/KS.png diff --git a/resources/games/cards/QC.png b/gwendolyn/resources/games/cards/QC.png similarity index 100% rename from resources/games/cards/QC.png rename to gwendolyn/resources/games/cards/QC.png diff --git a/resources/games/cards/QD.png b/gwendolyn/resources/games/cards/QD.png similarity index 100% rename from resources/games/cards/QD.png rename to gwendolyn/resources/games/cards/QD.png diff --git a/resources/games/cards/QH.png b/gwendolyn/resources/games/cards/QH.png similarity index 100% rename from resources/games/cards/QH.png rename to gwendolyn/resources/games/cards/QH.png diff --git a/resources/games/cards/QS.png b/gwendolyn/resources/games/cards/QS.png similarity index 100% rename from resources/games/cards/QS.png rename to gwendolyn/resources/games/cards/QS.png diff --git a/resources/games/cards/blue_back.png b/gwendolyn/resources/games/cards/blue_back.png similarity index 100% rename from resources/games/cards/blue_back.png rename to gwendolyn/resources/games/cards/blue_back.png diff --git a/resources/games/cards/gray_back.png b/gwendolyn/resources/games/cards/gray_back.png similarity index 100% rename from resources/games/cards/gray_back.png rename to gwendolyn/resources/games/cards/gray_back.png diff --git a/resources/games/cards/green_back.png b/gwendolyn/resources/games/cards/green_back.png similarity index 100% rename from resources/games/cards/green_back.png rename to gwendolyn/resources/games/cards/green_back.png diff --git a/resources/games/cards/purple_back.png b/gwendolyn/resources/games/cards/purple_back.png similarity index 100% rename from resources/games/cards/purple_back.png rename to gwendolyn/resources/games/cards/purple_back.png diff --git a/resources/games/cards/red_back.png b/gwendolyn/resources/games/cards/red_back.png similarity index 100% rename from resources/games/cards/red_back.png rename to gwendolyn/resources/games/cards/red_back.png diff --git a/resources/games/cards/yellow_back.png b/gwendolyn/resources/games/cards/yellow_back.png similarity index 100% rename from resources/games/cards/yellow_back.png rename to gwendolyn/resources/games/cards/yellow_back.png diff --git a/resources/games/deckofCards.txt b/gwendolyn/resources/games/deck_of_cards.txt similarity index 100% rename from resources/games/deckofCards.txt rename to gwendolyn/resources/games/deck_of_cards.txt diff --git a/gwendolyn/resources/help/help-add_movie.txt b/gwendolyn/resources/help/help-add_movie.txt new file mode 100644 index 0000000..fae39ff --- /dev/null +++ b/gwendolyn/resources/help/help-add_movie.txt @@ -0,0 +1 @@ +Du kan søge efter en film ved at skrive `/add_movie [søgning]`. Gwendolyn vil derefter vise dig resultater baseret på din søgning. Du kan derfra reagere på Gwendolyns besked for at downloade en specifik film. \ No newline at end of file diff --git a/resources/help/help-addmovie.txt b/gwendolyn/resources/help/help-add_show.txt similarity index 60% rename from resources/help/help-addmovie.txt rename to gwendolyn/resources/help/help-add_show.txt index daa1ac6..4868941 100644 --- a/resources/help/help-addmovie.txt +++ b/gwendolyn/resources/help/help-add_show.txt @@ -1 +1 @@ -Du kan søge efter en film ved at skrive `/addmovie [søgning]`. Gwendolyn vil derefter vise dig resultater baseret på din søgning. Du kan derfra reagere på Gwendolyns besked for at downloade en specifik film. \ No newline at end of file +Du kan søge efter et show ved at skrive `/add_show [søgning]`. Gwendolyn vil derefter vise dig resultater baseret på din søgning. Du kan derfra reagere på Gwendolyns besked for at downloade et specifikt show. \ No newline at end of file diff --git a/resources/help/help-balance.txt b/gwendolyn/resources/help/help-balance.txt similarity index 100% rename from resources/help/help-balance.txt rename to gwendolyn/resources/help/help-balance.txt diff --git a/resources/help/help-blackjack.txt b/gwendolyn/resources/help/help-blackjack.txt similarity index 100% rename from resources/help/help-blackjack.txt rename to gwendolyn/resources/help/help-blackjack.txt diff --git a/gwendolyn/resources/help/help-connect_four.txt b/gwendolyn/resources/help/help-connect_four.txt new file mode 100644 index 0000000..622dc19 --- /dev/null +++ b/gwendolyn/resources/help/help-connect_four.txt @@ -0,0 +1 @@ +Brug `/connect_four start` til at starte et spil imod Gwendolyn. Brug `/connect_four start [modstander]` for at spille imod en anden person. Du kan også bruge `/connect_four start [1-5]`, hvor tallet er sværhedsgraden af Gwendolyn du gerne vil spille imod. \ No newline at end of file diff --git a/resources/help/help-downloading.txt b/gwendolyn/resources/help/help-downloading.txt similarity index 88% rename from resources/help/help-downloading.txt rename to gwendolyn/resources/help/help-downloading.txt index 879ce89..b62f3dd 100644 --- a/resources/help/help-downloading.txt +++ b/gwendolyn/resources/help/help-downloading.txt @@ -1,4 +1,4 @@ -Viser dig de film og serier der er "requested" men ikke endnu på Bedre Netflix. Kommandoen kan tage imod op til 4 parametre: +Viser dig de film og serier der er "requested" men ikke endnu på Plex. Kommandoen kan tage imod op til 4 parametre: `-d`, `--downloading`, `--dm`, `--downloadManager` - Viser de torrents der er _ved_ at downloade. Hvis ingen parametre er givet, bliver det her parameter givet automatisk som det eneste. diff --git a/resources/help/help-give.txt b/gwendolyn/resources/help/help-give.txt similarity index 100% rename from resources/help/help-give.txt rename to gwendolyn/resources/help/help-give.txt diff --git a/resources/help/help-hangman.txt b/gwendolyn/resources/help/help-hangman.txt similarity index 100% rename from resources/help/help-hangman.txt rename to gwendolyn/resources/help/help-hangman.txt diff --git a/resources/help/help-hello.txt b/gwendolyn/resources/help/help-hello.txt similarity index 100% rename from resources/help/help-hello.txt rename to gwendolyn/resources/help/help-hello.txt diff --git a/resources/help/help-hex.txt b/gwendolyn/resources/help/help-hex.txt similarity index 100% rename from resources/help/help-hex.txt rename to gwendolyn/resources/help/help-hex.txt diff --git a/resources/help/help-image.txt b/gwendolyn/resources/help/help-image.txt similarity index 100% rename from resources/help/help-image.txt rename to gwendolyn/resources/help/help-image.txt diff --git a/resources/help/help-invest.txt b/gwendolyn/resources/help/help-invest.txt similarity index 100% rename from resources/help/help-invest.txt rename to gwendolyn/resources/help/help-invest.txt diff --git a/resources/help/help-monster.txt b/gwendolyn/resources/help/help-monster.txt similarity index 100% rename from resources/help/help-monster.txt rename to gwendolyn/resources/help/help-monster.txt diff --git a/gwendolyn/resources/help/help-movie.txt b/gwendolyn/resources/help/help-movie.txt new file mode 100644 index 0000000..6c072de --- /dev/null +++ b/gwendolyn/resources/help/help-movie.txt @@ -0,0 +1 @@ +Giver titlen på en tilfældig film fra Plex. \ No newline at end of file diff --git a/resources/help/help-name.txt b/gwendolyn/resources/help/help-name.txt similarity index 100% rename from resources/help/help-name.txt rename to gwendolyn/resources/help/help-name.txt diff --git a/resources/help/help-roll.txt b/gwendolyn/resources/help/help-roll.txt similarity index 100% rename from resources/help/help-roll.txt rename to gwendolyn/resources/help/help-roll.txt diff --git a/resources/help/help-spell.txt b/gwendolyn/resources/help/help-spell.txt similarity index 100% rename from resources/help/help-spell.txt rename to gwendolyn/resources/help/help-spell.txt diff --git a/gwendolyn/resources/help/help-star_wars_character.txt b/gwendolyn/resources/help/help-star_wars_character.txt new file mode 100644 index 0000000..a3a271b --- /dev/null +++ b/gwendolyn/resources/help/help-star_wars_character.txt @@ -0,0 +1 @@ + Du kan bruge kommandoer som `/star_wars_character name Jared` eller `/star_wars_character skills astrogation 3` til at ændre din karakters info. Kommandoen `/star_wars_character` vil give dig et character sheet for din karakter. \ No newline at end of file diff --git a/resources/help/help-starwarsroll.txt b/gwendolyn/resources/help/help-star_wars_roll.txt similarity index 86% rename from resources/help/help-starwarsroll.txt rename to gwendolyn/resources/help/help-star_wars_roll.txt index 4876bf2..f94e0e3 100644 --- a/resources/help/help-starwarsroll.txt +++ b/gwendolyn/resources/help/help-star_wars_roll.txt @@ -1 +1 @@ -Lader dig rulle Star Wars terninger. Du kan skrive tal der repræsenterer antallet af hver terning i rækkefølgen: ability, proficiency, difficulty, challenge, boost, setback og force. Du behøver ikke skrive et tal til alle terningerne. Du kan også skrive forbogstavet for terningen du vil rulle før antallet, såsom "/starWarsRoll f2", der ruller 2 force terninger. \ No newline at end of file +Lader dig rulle Star Wars terninger. Du kan skrive tal der repræsenterer antallet af hver terning i rækkefølgen: ability, proficiency, difficulty, challenge, boost, setback og force. Du behøver ikke skrive et tal til alle terningerne. Du kan også skrive forbogstavet for terningen du vil rulle før antallet, såsom "/star_wars_roll f2", der ruller 2 force terninger. \ No newline at end of file diff --git a/resources/help/help-tavern.txt b/gwendolyn/resources/help/help-tavern.txt similarity index 100% rename from resources/help/help-tavern.txt rename to gwendolyn/resources/help/help-tavern.txt diff --git a/resources/help/help-thank.txt b/gwendolyn/resources/help/help-thank.txt similarity index 100% rename from resources/help/help-thank.txt rename to gwendolyn/resources/help/help-thank.txt diff --git a/resources/help/help-trivia.txt b/gwendolyn/resources/help/help-trivia.txt similarity index 100% rename from resources/help/help-trivia.txt rename to gwendolyn/resources/help/help-trivia.txt diff --git a/resources/help/help-wolf.txt b/gwendolyn/resources/help/help-wolf.txt similarity index 100% rename from resources/help/help-wolf.txt rename to gwendolyn/resources/help/help-wolf.txt diff --git a/resources/help/help.txt b/gwendolyn/resources/help/help.txt similarity index 71% rename from resources/help/help.txt rename to gwendolyn/resources/help/help.txt index 9501046..74da75f 100644 --- a/resources/help/help.txt +++ b/gwendolyn/resources/help/help.txt @@ -3,22 +3,22 @@ `/spell` - Slå en besværgelse op. `/monster` - Slå et monster op. `/image` - Finder et tilfældigt billede fra internettet. -`/movie` - Giver titlen på en tilfældig film fra Bedre Netflix +`/movie` - Giver titlen på en tilfældig film fra Plex `/name` - Genererer et tilfældigt navn. `/tavern` - Genererer en tilfældig tavern. `/give` - Lader dig give GwendoBucks til andre. -`/starWarsCharacter` - Lader dig lave en Star Wars karakter. -`/starWarsRoll` - Lader dig rulle Star Wars terninger. +`/star_wars_character` - Lader dig lave en Star Wars karakter. +`/star_wars_roll` - Lader dig rulle Star Wars terninger. `/balance` - Viser dig hvor mange GwendoBucks du har. `/invest` - Lader dig investere dine GwendoBucks i aktiemarkedet. `/blackjack` - Lader dig spille et spil blackjack. `/trivia` - Lader dig spille et spil trivia, hvor du kan tjene GwendoBucks. -`/connectFour` - Lader dig spille et spil fire på stribe. +`/connect_four` - Lader dig spille et spil fire på stribe. `/hex` - Lader dig spille et spil Hex. `/hangman` - Lader dig spille et spil hangman. `/wolf` - Lader dig slå ting op på Wolfram Alpha. -`/addmovie` - Lader dig tilføje film til Bedre Netflix. -`/addshow` - Lader dig tilføje tv shows til Bedre Netflix. +`/add_movie` - Lader dig tilføje film til Plex. +`/add_show` - Lader dig tilføje tv shows til Plex. `/downloading` - Viser dig hvor langt de torrents der er ved at downloade er kommet. `/thank` - Lader dig takke Gwendolyn. Du kan få ekstra information om kommandoerne med "/help [kommando]". diff --git a/gwendolyn/resources/long_strings.json b/gwendolyn/resources/long_strings.json new file mode 100644 index 0000000..c5964a3 --- /dev/null +++ b/gwendolyn/resources/long_strings.json @@ -0,0 +1,27 @@ +{ + "missing parameters" : "Missing command parameters. Try using `!help [command]` to find out how to use the command.", + "Can't log in": "Could not log in. Remember to write your bot token in the credentials.txt file", + "Blackjack all players standing": "All players are standing. The dealer now shows his cards and draws.", + "Blackjack first round": ". You can also double down with \"/blackjack double\" or split with \"/blackjack split\"", + "Blackjack commands": "You have 2 minutes to either hit or stand with \"/blackjack hit\" or \"/blackjack stand\"{}. It's assumed you're standing if you don't make a choice.", + "Blackjack double": "Adding another {} GwendoBucks to {}'s bet and drawing another card.", + "Blackjack different cards": "You can only split if your cards have the same value", + "Blackjack split": "Splitting {}'s hand into 2. Adding their original bet to the second hand. You can use \"/blackjack hit/stand/double 1\" and \"/blackjack hit/stand/double 2\" to play the different hands.", + "Blackjack started": "Blackjack game started. Use \"/blackjack bet [amount]\" to enter the game within the next 30 seconds.", + "Blackjack going on": "There's already a blackjack game going on. Try again in a few minutes.", + "Stock value": "The current {} stock is valued at **{}** GwendoBucks", + "Stock parameters": "You must give both a stock name and an amount of GwendoBucks you wish to spend.", + "Trivia going on": "There's already a trivia question going on. Try again in like, a minute", + "Trivia time up": "Time's up! The answer was \"*{}) {}*\". Anyone who answered that has gotten 1 GwendoBuck", + "Connect 4 going on": "There's already a connect 4 game going on in this channel", + "Connect 4 placed": "{} placed a piece in column {}. It's now {}'s turn", + "Hangman going on": "There's already a Hangman game going on in the channel", + "Hangman lost game": " You've guessed wrong six times and have lost the game.", + "Hangman guessed word": " You've guessed the word! Congratulations! Adding 15 GwendoBucks to your account", + "Already on Plex": "{} is either already on Plex, downloading, or not available", + "No torrent": "{}No torrent exists. Likely because the movie is not yet released on DVD", + "No torrents downloading": "There are no torrents downloading right. If the torrent you're looking for was added more than 24 hours ago, it might already be on Plex.", + "Update": "{}\nThis message will update every 10 seconds for {} more minutes\n```", + "No updates": "{}\nThis message will not update anymore\n```", + "Invalid parameters": "Invalid or repeated parameters. Use '/help downloading' to see valid parameters." +} \ No newline at end of file diff --git a/resources/paper.jpg b/gwendolyn/resources/paper.jpg similarity index 100% rename from resources/paper.jpg rename to gwendolyn/resources/paper.jpg diff --git a/resources/slashParameters.json b/gwendolyn/resources/slash_parameters.json similarity index 90% rename from resources/slashParameters.json rename to gwendolyn/resources/slash_parameters.json index 876bc46..275b2e9 100644 --- a/resources/slashParameters.json +++ b/gwendolyn/resources/slash_parameters.json @@ -1,7 +1,7 @@ { - "addMovie" : { - "name" : "addMovie", - "description" : "Request a movie for Bedre Netflix", + "add_movie" : { + "name" : "add_movie", + "description" : "Request a movie for Plex", "options" : [ { "name" : "movie", @@ -11,9 +11,9 @@ } ] }, - "addShow" : { - "name" : "addShow", - "description" : "Request a show for Bedre Netflix", + "add_show" : { + "name" : "add_show", + "description" : "Request a show for Plex", "options" : [ { "name" : "show", @@ -27,7 +27,7 @@ "name" : "balance", "description" : "See your balance of GwendoBucks" }, - "blackjackBet" : { + "blackjack_bet" : { "base" : "blackjack", "name" : "bet", "description" : "Enter the current blackjack game with a bet", @@ -40,12 +40,12 @@ } ] }, - "blackjackCards" : { + "blackjack_cards" : { "base" : "blackjack", "name" : "cards", "description" : "Get a count of the cards used in blackjack games" }, - "blackjackDouble" : { + "blackjack_double" : { "base" : "blackjack", "name" : "double", "description" : "Double your bet in blackjack", @@ -58,12 +58,12 @@ } ] }, - "blackjackHilo" : { + "blackjack_hilo" : { "base" : "blackjack", "name" : "hilo", "description" : "Get the current hi-lo value for the cards used in blackjack games" }, - "blackjackHit" : { + "blackjack_hit" : { "base" : "blackjack", "name" : "hit", "description" : "Hit on your hand in blackjack", @@ -76,12 +76,12 @@ } ] }, - "blackjackShuffle" : { + "blackjack_shuffle" : { "base" : "blackjack", "name" : "shuffle", "description" : "Shuffle the cards used in blackjack games" }, - "blackjackSplit" : { + "blackjack_split" : { "base" : "blackjack", "name" : "split", "description" : "Split your hand in blackjack", @@ -94,7 +94,7 @@ } ] }, - "blackjackStand" : { + "blackjack_stand" : { "base" : "blackjack", "name" : "stand", "description" : "Stand on your hand in blackjack", @@ -107,13 +107,13 @@ } ] }, - "blackjackStart" : { + "blackjack_start" : { "base" : "blackjack", "name" : "start", "description" : "Start a game of blackjack" }, - "connectFourStartGwendolyn" : { - "base" : "connectFour", + "connect_four_start_gwendolyn" : { + "base" : "connect_four", "subcommand_group" : "start", "name" : "Gwendolyn", "description" : "Start a game of connect four against Gwendolyn", @@ -126,8 +126,8 @@ } ] }, - "connectFourStartUser" : { - "base" : "connectFour", + "connect_four_start_user" : { + "base" : "connect_four", "subcommand_group" : "start", "name" : "user", "description" : "Start a game of connect four against another user", @@ -140,14 +140,14 @@ } ] }, - "connectFourSurrender" : { - "base" : "connectFour", + "connect_four_surrender" : { + "base" : "connect_four", "name" : "surrender", "description" : "Surrender the game of connect four" }, "downloading" : { "name" : "downloading", - "description" : "See current downloads for Bedre Netflix", + "description" : "See current downloads for Plex", "options" : [ { "name" : "parameters", @@ -162,7 +162,7 @@ "description" : "Set the 'playing' text for Gwendolyn", "options" : [ { - "name" : "gameText", + "name" : "game_text", "description" : "The game to set the 'playing' text to", "type" : 3, "required" : "true" @@ -187,12 +187,12 @@ } ] }, - "hangmanStart" : { + "hangman_start" : { "base" : "hangman", "name" : "start", "description" : "Start a game of hangman" }, - "hangmanStop" : { + "hangman_stop" : { "base" : "hangman", "name" : "stop", "description" : "Stop the current game of hangman" @@ -213,7 +213,7 @@ } ] }, - "hexPlace" : { + "hex_place" : { "base" : "hex", "name" : "place", "description" : "Place a piece on the hex board", @@ -226,7 +226,7 @@ } ] }, - "hexStartGwendolyn" : { + "hex_start_gwendolyn" : { "base" : "hex", "subcommand_group" : "start", "name" : "Gwendolyn", @@ -240,7 +240,7 @@ } ] }, - "hexStartUser" : { + "hex_start_user" : { "base" : "hex", "subcommand_group" : "start", "name" : "user", @@ -254,17 +254,17 @@ } ] }, - "hexSurrender" : { + "hex_surrender" : { "base" : "hex", "name" : "surrender", "description" : "Surrender the game of hex" }, - "hexSwap" : { + "hex_swap" : { "base" : "hex", "name" : "swap", "description" : "Perform a hex swap" }, - "hexUndo" : { + "hex_undo" : { "base" : "hex", "name" : "undo", "description" : "Undo your last hex move" @@ -333,8 +333,8 @@ } ] }, - "starWarsCharacter" : { - "name" : "starWarsCharacter", + "star_wars_character" : { + "name" : "star_wars_character", "description" : "Manage your Star Wars character sheet", "options" : [ { @@ -345,8 +345,8 @@ } ] }, - "starWarsCrit" : { - "name" : "starWarsCrit", + "star_wars_crit" : { + "name" : "star_wars_crit", "description" : "Roll a Star Wars critical injury", "options" : [ { @@ -357,8 +357,8 @@ } ] }, - "starWarsDestiny" : { - "name" : "starWarsDestiny", + "star_wars_destiny" : { + "name" : "star_wars_destiny", "description" : "Use and see Star Wars Destiny points", "options" : [ { @@ -369,8 +369,8 @@ } ] }, - "starWarsRoll" : { - "name" : "starWarsRoll", + "star_wars_roll" : { + "name" : "star_wars_roll", "description" : "Roll Star Wars dice", "options" : [ { diff --git a/resources/starWars/starwarsskills.json b/gwendolyn/resources/star_wars/starwarsskills.json similarity index 100% rename from resources/starWars/starwarsskills.json rename to gwendolyn/resources/star_wars/starwarsskills.json diff --git a/resources/starWars/starwarstemplates.json b/gwendolyn/resources/star_wars/starwarstemplates.json similarity index 100% rename from resources/starWars/starwarstemplates.json rename to gwendolyn/resources/star_wars/starwarstemplates.json diff --git a/resources/startingFiles.json b/gwendolyn/resources/starting_files.json similarity index 82% rename from resources/startingFiles.json rename to gwendolyn/resources/starting_files.json index b657456..78affaa 100644 --- a/resources/startingFiles.json +++ b/gwendolyn/resources/starting_files.json @@ -1,6 +1,6 @@ { "json":{ - "resources/lookup/spells.json" : { + "gwendolyn/resources/lookup/spells.json" : { "Fireball" : { "casting_time" : "1 action", "components" : "V, S, M (a tiny ball of bat guano and sulfur)", @@ -12,7 +12,7 @@ "ritual" : false } }, - "resources/lookup/monsters.json" : [ + "gwendolyn/resources/lookup/monsters.json" : [ { "name": "Bandit", "size": "Medium", @@ -56,19 +56,19 @@ ] }, "txt": { - "resources/starWars/destinyPoints.txt": "", - "resources/movies.txt": "The Room", - "resources/names.txt": "Gandalf", + "gwendolyn/resources/star_wars/destinyPoints.txt": "", + "gwendolyn/resources/movies.txt": "The Room", + "gwendolyn/resources/names.txt": "Gandalf\n", "credentials.txt" : "Bot token: TOKEN\nFinnhub API key: KEY\nWordnik API Key: KEY\nMongoDB user: USERNAME\nMongoDB password: PASSWORD\nWolframAlpha AppID: APPID\nRadarr API key: KEY\nSonarr API key: KEY", "options.txt" : "Testing: True\nTesting guild ids:\nAdmins:" }, "folder" : [ - "resources/lookup", - "resources/games/blackjackTables", - "resources/games/connect4Boards", - "resources/games/hexBoards", - "resources/games/hangmanBoards", - "resources/bedreNetflix", - "resources/games/oldImages" + "gwendolyn/resources/lookup", + "gwendolyn/resources/games/blackjack_tables", + "gwendolyn/resources/games/connect_four_boards", + "gwendolyn/resources/games/hex_boards", + "gwendolyn/resources/games/hangman_boards", + "gwendolyn/resources/plex", + "gwendolyn/resources/games/old_images" ] } \ No newline at end of file diff --git a/gwendolyn/utils/__init__.py b/gwendolyn/utils/__init__.py new file mode 100644 index 0000000..863b018 --- /dev/null +++ b/gwendolyn/utils/__init__.py @@ -0,0 +1,11 @@ +"""A collections of utilities used by Gwendolyn and her functions.""" + +__all__ = ["get_options", "get_credentials", "DatabaseFuncs", "EventHandler", + "ErrorHandler", "get_params", "log_this", "cap", "make_files", + "replace_multiple", "emoji_to_command"] + +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) diff --git a/gwendolyn/utils/event_handlers.py b/gwendolyn/utils/event_handlers.py new file mode 100644 index 0000000..ca8093b --- /dev/null +++ b/gwendolyn/utils/event_handlers.py @@ -0,0 +1,177 @@ +""" +Classes used to handle bot events and errors. + +*Classes* +--------- + EventHandler + 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 + + +class EventHandler(): + """ + Handles bot events. + + *Methods* + --------- + on_ready() + on_slash_command(ctx: discord_slash.context.SlashContext) + on_reaction_add(ctx: discord_slash.context.SlashContext) + """ + + def __init__(self, bot): + """Initialize the handler.""" + self.bot = bot + + async def on_ready(self): + """Log and sets status when it logs in.""" + await self.bot.database_funcs.imdb_commands() + name = self.bot.user.name + userid = str(self.bot.user.id) + logged_in_message = f"Logged in as {name}, {userid}" + self.bot.log(logged_in_message, level=25) + game = discord.Game("Use /help for commands") + + online_status = discord.Status.online + await self.bot.change_presence(activity=game, status=online_status) + + async def on_slash_command(self, ctx: SlashContext): + """Log when a slash command is given.""" + if ctx.subcommand_name is not None: + subcommand = f" {ctx.subcommand_name} " + else: + subcommand = " " + + if ctx.subcommand_group is not None: + sub_command_group = f"{ctx.subcommand_group} " + else: + sub_command_group = "" + + args = " ".join([str(i) for i in ctx.args]) + full_command = f"/{ctx.command}{subcommand}{sub_command_group}{args}" + 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). + + reaction_test_parameters = [message, f"#{str(user.id)}"] + + 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) + + 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] + + 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] + + 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(): + """ + Handles errors. + + *Methods* + --------- + on_slash_command_error(ctx: discord_slash.context.SlashContext, + error: Exception) + on_error(method: str) + """ + + def __init__(self, bot): + """Initialize the handler.""" + self.bot = bot + + async def on_slash_command_error(self, ctx: SlashContext, + error: Exception): + """Log when there's a slash command.""" + if isinstance(error, commands.CommandNotFound): + await ctx.send("That's not a command") + elif isinstance(error, discord.errors.NotFound): + self.bot.log("Deleted message before I could add all reactions") + elif isinstance(error, commands.errors.MissingRequiredArgument): + self.bot.log(f"{error}", str(ctx.channel_id)) + await ctx.send(self.bot.long_strings["missing parameters"]) + else: + params = [type(error), error, error.__traceback__] + exception = traceback.format_exception(*params) + + exception_string = "".join(exception) + log_messages = [f"exception in /{ctx.name}", f"{exception_string}"] + self.bot.log(log_messages, str(ctx.channel_id), 40) + if isinstance(error, discord.errors.NotFound): + self.bot.log("Context is non-existant", level=40) + else: + await ctx.send("Something went wrong (error code 000)") + + 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_string = "".join(exception) + log_messages = [f"exception in {method}", f"{exception_string}"] + self.bot.log(log_messages, level=40) diff --git a/gwendolyn/utils/helper_classes.py b/gwendolyn/utils/helper_classes.py new file mode 100644 index 0000000..8f9f535 --- /dev/null +++ b/gwendolyn/utils/helper_classes.py @@ -0,0 +1,260 @@ +""" +Contains classes used for utilities. + +*Classes* +--------- + 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 +import discord # Used for type hints + + +class DatabaseFuncs(): + """ + Manages database functions. + + *Methods* + --------- + get_name(user_id: str) -> str + get_id(user_name: str) -> str + delete_game(game_type: str, channel: str) + 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() + """ + + def __init__(self, bot): + """Initialize the class.""" + self.bot = bot + + def get_name(self, user_id: str): + """ + Get the name of a user you have the # id of. + + *Parameters: + ------------ + user_id: str + The id of the user you want the name of. The format is + "#" + str(discord.User.id) + + *Returns* + --------- + user_name: str + The name of the user. If the user couldn't be found, + returns the user_id. + """ + user = self.bot.database["users"].find_one({"_id": user_id}) + + if user_id == f"#{self.bot.user.id}": + return_name = "Gwendolyn" + elif user is not None: + return_name = user["user name"] + else: + self.bot.log(f"Couldn't find user {user_id}") + return_name = user_id + + return return_name + + def get_id(self, user_name: str): + """ + Get the id of a user you have the username of. + + *Parameters: + ------------ + user_name: str + The name of the user you want the id of. + + *Returns* + --------- + user_id: str + The id of the user in the format "#" + + str(discord.User.id). If the user couldn't be found, + returns the user_name. + """ + user_search = {"user name": re.compile(user_name, re.IGNORECASE)} + user = self.bot.database["users"].find_one(user_search) + + if user is not None: + return_id = user["_id"] + else: + self.bot.log("Couldn't find user "+user_name) + return_id = None + + return return_id + + def delete_game(self, game_type: str, channel: str): + """ + Remove a game from the database. + + *Parameters* + ------------ + game_type: str + The name of the collection the game is in, like + "hangman games", "blackjack games" etc. + channel: str + The channel id of the channel the game is on as a + string. + """ + self.bot.database[game_type].delete_one({"_id": channel}) + + def wipe_games(self): + """Delete all running games and pull from git.""" + game_types = [ + "trivia questions", + "blackjack games", + "connect 4 games", + "hangman games", + "hex games" + ] + for game_type in game_types: + self.bot.database[game_type].delete_many({}) + + def connect_four_reaction_test(self, message: discord.Message, + user: discord.User): + """ + Test if the given message is the current connect four game. + + Also tests if the given user is the one who's turn it is. + + *Parameters* + ------------ + message: discord.Message + The message to test. + user: discord.User + The user to test. + *Returns* + --------- + : bool + Whether the given message is the current connect four + game and if the user who reacted is the user who's turn + it is. + """ + channel = message.channel + channel_search = {"_id": str(channel.id)} + game = self.bot.database["connect 4 games"].find_one(channel_search) + + 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: + old_image = int(file_pointer.read()) + else: + old_image = 0 + + if message.id == old_image: + self.bot.log("They reacted to the connect_four game") + turn = game["turn"] + if user == game["players"][turn]: + valid_reaction = True + else: + self.bot.log("It wasn't their turn") + valid_reaction = False + else: + valid_reaction = False + + 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"] + last_synced = collection.find_one() + now = time.time() + if last_synced["last synced"] < now - 86400: + slash_command_list = await self.bot.slash.to_dict() + self.bot.log(f"Updating commands: {slash_command_list}") + await self.bot.slash.sync_all_commands() + id_number = last_synced["_id"] + query_filter = {"_id": id_number} + update = {"$set": {"last synced": now}} + collection.update_one(query_filter, update) diff --git a/gwendolyn/utils/util_functions.py b/gwendolyn/utils/util_functions.py new file mode 100644 index 0000000..d6f62b3 --- /dev/null +++ b/gwendolyn/utils/util_functions.py @@ -0,0 +1,340 @@ +""" +Contains utility functions used by parts of the bot. + +*Functions* +----------- + sanitize(data: str, lower_case_value: bool = false) -> dict + get_options() -> dict + get_credentials() -> dict + long_strings() -> dict + get_params() -> dict + log_this(messages: Union[str, list], channel: str = "", + level: int = 20) + cap(s: str) -> str + make_files() + replace_multiple(main_string: str, to_be_replaced: list, + new_string: str) -> str + emoji_to_command(emoji: str) -> str +""" +import json # Used by long_strings(), get_params() and make_files() +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 + +# All of this is logging configuration +FORMAT = " %(asctime)s | %(name)-16s | %(levelname)-8s | %(message)s" +PRINTFORMAT = "%(asctime)s - %(message)s" +DATEFORMAT = "%Y-%m-%d %H:%M:%S" + +logging.addLevelName(25, "PRINT") +loggingConfigParams = { + "format": FORMAT, + "datefmt": DATEFORMAT, + "level": logging.INFO, + "filename": "gwendolyn.log" +} +logging.basicConfig(**loggingConfigParams) +logger = logging.getLogger("Gwendolyn") +printer = logging.getLogger("printer") +handler = logging.StreamHandler(sys.stdout) +handler.setFormatter(logging.Formatter(fmt=PRINTFORMAT, datefmt=DATEFORMAT)) +printer.addHandler(handler) +printer.propagate = False + +imdb._logging.setLevel("CRITICAL") # pylint: disable=protected-access +# Basically disables imdbpy logging, since it's being printed to the +# terminal. + +def sanitize(data: str, lower_case_value: bool = False): + """ + Sanitize and create a dictionary from a string. + + Each element is created from a line with a : in it. The key is left + of the :, the value is right of it. + + *Parameters* + ------------ + data: str + The string to create a dict from. + lower_case_value: bool = False + Whether the value of each element should be lowercase. + + *Returns* + --------- + dct: dict + The sanitized dictionary of elements. + + """ + data = data.splitlines() + dct = {} + for line in data: + if line[0] != "#" and ":" in line: + line_values = line.split(":") + line_values[0] = line_values[0].lower() + line_values[1] = line_values[1].replace(" ", "") + if lower_case_value: + line_values[1] = line_values[1].lower() + + if line_values[0] in ["testing guild ids", "admins"]: + line_values[1] = line_values[1].split(",") + if all(i.isnumeric() for i in line_values[1]): + line_values[1] = [int(i) for i in line_values[1]] + + if any(i == line_values[1] for i in ["true", "false"]): + line_values[1] = (line_values[1] == "true") + + dct[line_values[0]] = line_values[1] + + return dct + + +def get_options(): + """ + Get the bot options as dict. + + *Returns* + --------- + options: dict + The options of the bot. + """ + with open("options.txt", "r") as file_pointer: + data = sanitize(file_pointer.read(), True) + + options = {} + + options["testing"] = data["testing"] + options["guild_ids"] = data["testing guild ids"] + options["admins"] = data["admins"] + return options + +def get_credentials(): + """ + Returns the credentials used by the bot as a dict. + + *Returns* + --------- + credentials: dict + The credentials used by the bot. + """ + with open("credentials.txt", "r") as file_pointer: + data = sanitize(file_pointer.read()) + + credentials = {} + + credentials["token"] = data["bot token"] + credentials["finnhub_key"] = data["finnhub api key"] + credentials["wordnik_key"] = data["wordnik api key"] + credentials["mongo_db_user"] = data["mongodb user"] + credentials["mongo_db_password"] = data["mongodb password"] + credentials["wolfram_alpha_key"] = data["wolframalpha appid"] + credentials["radarr_key"] = data["radarr api key"] + credentials["sonarr_key"] = data["sonarr api key"] + + return credentials + +def long_strings(): + """ + Get the data from gwendolyn/resources/long_strings.json. + + *Returns* + --------- + data: dict + The long strings and their keys. + """ + with open("gwendolyn/resources/long_strings.json", "r") as file_pointer: + data = json.load(file_pointer) + + return data + + +def get_params(): + """ + Get the slash command parameters. + + *Returns* + --------- + params: dict + The parameters for every slash command. + """ + with open("gwendolyn/resources/slash_parameters.json", "r") as file_pointer: + slash_parameters = json.load(file_pointer) + + options = get_options() + + if options["testing"]: + for parameter in slash_parameters: + slash_parameters[parameter]["guild_ids"] = options["guild_ids"] + + return slash_parameters + + +def log_this(messages, channel: str = "", level: int = 20): + """ + Log something in Gwendolyn's logs. + + *Parameters* + ------------ + messages: Union[str, list] + A string or list of strings to be logged. If there are + multiple strings and the level is PRINT (25) or higher, + only the first string will be printed. + channel: str = "" + The channel the event to be logged occurred in. Will be + logged along with the message(s). + level: int = 20 + The level to log the message(s) at. If PRINT (25) or + higher, the first message will be printed to the console. + """ + channel = channel.replace("Direct Message with ", "") + if isinstance(messages, str): + messages = [messages] + + print_message = messages[0] + + for i, message in enumerate(messages): + if channel != "": + messages[i] = f"{message} - ({channel})" # Adds channel ID + # to log messages + + if len(messages) > 1: # Tells user to check the log if there are + # more messages there + print_message += " (details in log)" + + if level >= 25: + printer.log(level, print_message) + + for log_message in messages: + logger.log(level, log_message) + + +def cap(input_string: str): + """ + Capitalize a string like a movie title. + + That means "of" and "the" are not capitalized. + + *Parameters* + ------------ + input_string: str + The string to capitalized. + + *Returns* + --------- + return_string: str + The capitalized string. + """ + no_caps_list = ["of", "the"] + word_number = 0 + string_list = input_string.split() + return_string = '' + for word in string_list: + word_number += 1 + if word not in no_caps_list or word_number == 1: + word = word.capitalize() + return_string += word+" " + return_string = return_string[:-1] + return return_string + + +def make_files(): + """Create all the files and directories needed by Gwendolyn.""" + def make_json_file(path, content): + """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: + 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: + file_pointer.write(content) + + def directory(path): + """Create directory if it doesn't exist.""" + if not os.path.isdir(path): + os.makedirs(path) + log_this("The "+path.split("/")[-1]+" directory didn't exist") + + with open("gwendolyn/resources/starting_files.json") as file_pointer: + data = json.load(file_pointer) + + for path, content in data["json"].items(): + make_json_file(path, content) + + for path, content in data["txt"].items(): + make_txt_file(path, content) + + for path in data["folder"]: + directory(path) + + +def replace_multiple(main_string: str, to_be_replaced: list, new_string: str): + """ + Replace multiple substrings in a string with the same substring. + + *Parameters* + ------------ + main_string: str + The string to replace substrings in. + to_be_replaced: list + The substrings to replace. + new_string: str + The string to replace the substrings with. + + *Returns* + --------- + main_string: str + The string with the substrings replaced. + """ + # Iterate over the strings to be replaced + for elem in to_be_replaced: + # Check if string is in the main string + if elem in main_string: + # Replace the string + main_string = main_string.replace(elem, new_string) + + return main_string + + +def emoji_to_command(emoji: str): + """ + Convert emoji to text. + + *Parameters* + ------------ + emoji: str + The emoji to decipher. + + *Returns* + --------- + : str + The deciphered string. + """ + if emoji == "1️⃣": + return_value = 1 + elif emoji == "2️⃣": + return_value = 2 + elif emoji == "3️⃣": + return_value = 3 + elif emoji == "4️⃣": + return_value = 4 + elif emoji == "5️⃣": + return_value = 5 + elif emoji == "6️⃣": + return_value = 6 + elif emoji == "7️⃣": + return_value = 7 + elif emoji == "🎲": + return_value = "roll" + elif emoji == "❌": + return_value = "none" + elif emoji == "✔️": + return_value = 1 + else: + return_value = "" + + return return_value diff --git a/main.py b/main.py new file mode 100644 index 0000000..ba3dee2 --- /dev/null +++ b/main.py @@ -0,0 +1,30 @@ +"""Runs the Gwendolyn bot.""" + +import platform # Used to test if the bot is running on windows, in + # order to fix a bug with asyncio +import asyncio # used to set change the loop policy if the bot is + # running on windows + +from gwendolyn import Gwendolyn +from gwendolyn.utils import make_files + + +def main(): + """Starts the bot""" + if platform.system() == "Windows": + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + + # Creates the required files + make_files() + + # Creates the Bot + bot = Gwendolyn() + + try: + # Runs the whole shabang + bot.run(bot.credentials["token"]) + except Exception as exception: # pylint: disable=broad-except + bot.log(bot.long_strings[f"Can't log in: {repr(exception)}"]) + +if __name__ == "__main__": + main() diff --git a/project-guidelines.md b/project-guidelines.md index 212f61c..583932d 100644 --- a/project-guidelines.md +++ b/project-guidelines.md @@ -42,8 +42,8 @@ Comments, strings, variable names, class names, docstrings, as well as all other # Code ## Code Style -All the Python code should follow the [PEP 8 guidelines](https://www.python.org/dev/peps/pep-0008/), with the following differences: -+ Variable and function names must be camelCase, and must fully consist of either full words or common/understandable abbreviations. +All the Python code should follow the [PEP 8 guidelines](https://www.python.org/dev/peps/pep-0008/), with the following additions: ++ Variable and function names must fully consist of either full words or common/understandable abbreviations. + Use f-strings when applicable. ### Documentation @@ -91,7 +91,7 @@ The `Command` methods in cogs should only exist to perform small tasks or call c ### Folders + `cogs/` contains the command cogs. + `funcs/` contains all functions and classes called on to perform commands. All functions must be accessible through a class, of which the `Gwendolyn` class has an instance as an attribute. -+ `resources/` contains the images, lookup databases, fonts etc. that the rest of the code uses. ++ `gwendolyn/resources/` contains the images, lookup databases, fonts etc. that the rest of the code uses. ### Important files + `Gwendolyn.py` contains the Gwendolyn class and running it starts the bot. It should be lightweight and not more than 100 lines. @@ -108,6 +108,6 @@ Things you should know about the logging: + Logs of level `ERROR` should only be created in the `on_command_error()`, `on_error()` or `Command.error()` functions. ### Logging rules -1. Never call the `logThis()` function from `/utils/utilFuncs/`. Always call `bot.log`. +1. Never call the `log_this()` function from `/utils/utilFuncs/`. Always call `bot.log`. 1. The `on_slash_command()` and `on_ready()` events are the only times log should be called at level 25. `INFO` level logs should be used for all other logging. 1. Always provide the channel id if available. Although you shouldn't pass the channel id to a function purely to use it in logs. diff --git a/requirements.txt b/requirements.txt index e66d93c..a46b098 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,10 +8,10 @@ certifi==2020.12.5 chardet==4.0.0 colorama==0.4.4 d20==1.1.0 -discord-py-slash-command==1.1.1 +discord-py-slash-command==1.1.2 discord.py==1.7.1 dnspython==2.1.0 -docutils==0.16 +docutils==0.17 fandom-py==0.2.1 finnhub-python==2.4.0 gitdb==4.0.7 diff --git a/resources/errorCodes.txt b/resources/errorCodes.txt deleted file mode 100644 index 9234601..0000000 --- a/resources/errorCodes.txt +++ /dev/null @@ -1,128 +0,0 @@ -000 - Unspecified error -001 - Not a command - -1 - Help -100 - Unspecified error -101 - Couldn't find help.txt -102 - Couldn't find help file for specified command - -2 - Stop -200 - Unspecified error -201 - Unauthorized user - -3 - Simple Commands -310 - Hello error -320 - Map error -330 - Name error -340 - Tavern error -350 - Game error - -4 - Roll -400 - Unspecified error - -5 - Spell -500 - Unspecified error -501 - Spell not in database - -6 - Monster -600 - Unspecified error -601 - Monster name too short -602 - Monster not in database - -7 - Image -700 - Unspecified error -701 - Can't connect to Bing -702 - Error picking camera type/image name - -8 - Movie -800 - Unspecified error -801 - Error in function -802 - Can't find movie on imdb -803 - Can't extract data -804 - Can't pick movie -805 - Embed error - -9 - Star Wars -910 - Unspecified swroll error -911 - Obligation roll fucked up -912 - No character swroll -913 - Didn't include heavy/light in ranged or planetary/space in piloting -914 - Swroll invalid input -920 - Unspecified swd error -921 - Didn't specify amount of players -922 - Invalid input -930 - Unspecified swcrit -931 - Swcrit didn't include a number -940 - Unspecified swchar error -941 - No weapons -942 - Can't add to character sheet -943 - No character swchar -944 - Not on character sheet -945 - Problem overwriting data -946 - Problem removing data -947 - Problem adding data -948 - Problem removing spaces -949 - Wrong data type - -10 - Wiki -1000 - Unspecified error -1001 - Something fucked up -1002 - Can't find page - -11 - Trivia -1100 - Unspecified error -1101 - Incorrect input -1102 - Can't find question -1103 - Not an answer -1104 - No question going on -1105 - User has already answered -1106 - There's already a question goin on in the channel - -12 - Money -1210 - Unspecified balance error -1220 - Unspecified give error -1221 - Conversion error -1222 - Incorrect input -1223a - User has no money -1223b - Not enough money - -13 - Blackjack -1300 - Unspecified error -1310 - Unspecified finishing error -1311 - Error calculating winnings -1312 - Error in calcWinnings function -1320 - Unspecified loop error -1321 - Loop interrupted while waiting -1322 - Error with getHandNumber() -1330 - Unspecified continue error -1331 - Error in testIfStanding() -1340 - Error in drawing blackjack table -1341 - Error in drawHand() - -14 - connect four -1400 - Unspecified error -1401 - Error deleting old image -1410 - Unspecified parsing error -1420 - Unspecified AI error - -15 - Hex -1500 - Unspecified -1501 - Error deleting old image -1510 - Unspecified parsing error -1520 - Unspecified AI error -1531 - Invalid position -1532 - Cannot place on existing piece -1533 - Position out of bounds -1541 - Error loading board-image -1542 - Error swapping - -17 - Hangman -1700 - Unspecified error -1701 - Error parsing command -1710 - Error in drawImage() -1711 - Error in drawGallows() -1712 - Error in drawMan() -1713 - Error in drawLetterLines() -1714 - Error in drawMisses() -1720 - Unspecified hangmanGuess() error -1730 - Unspecified hangmanStart() error diff --git a/resources/help/help-addshow.txt b/resources/help/help-addshow.txt deleted file mode 100644 index 9d35d5c..0000000 --- a/resources/help/help-addshow.txt +++ /dev/null @@ -1 +0,0 @@ -Du kan søge efter et show ved at skrive `/addshow [søgning]`. Gwendolyn vil derefter vise dig resultater baseret på din søgning. Du kan derfra reagere på Gwendolyns besked for at downloade et specifikt show. \ No newline at end of file diff --git a/resources/help/help-connectfour.txt b/resources/help/help-connectfour.txt deleted file mode 100644 index ee48f68..0000000 --- a/resources/help/help-connectfour.txt +++ /dev/null @@ -1 +0,0 @@ -Brug `/connectFour start` til at starte et spil imod Gwendolyn. Brug `/connectFour start [modstander]` for at spille imod en anden person. Du kan også bruge `/connectFour start [1-5]`, hvor tallet er sværhedsgraden af Gwendolyn du gerne vil spille imod. \ No newline at end of file diff --git a/resources/help/help-movie.txt b/resources/help/help-movie.txt deleted file mode 100644 index 8fa0eed..0000000 --- a/resources/help/help-movie.txt +++ /dev/null @@ -1 +0,0 @@ -Giver titlen på en tilfældig film fra Bedre Netflix. \ No newline at end of file diff --git a/resources/help/help-starwarscharacter.txt b/resources/help/help-starwarscharacter.txt deleted file mode 100644 index 794add6..0000000 --- a/resources/help/help-starwarscharacter.txt +++ /dev/null @@ -1 +0,0 @@ - Du kan bruge kommandoer som `/starWarsCharacter name Jared` eller `/starWarsCharacter skills astrogation 3` til at ændre din karakters info. Kommandoen `/starWarsCharacter` vil give dig et character sheet for din karakter. \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py deleted file mode 100644 index 7d2a46c..0000000 --- a/utils/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -"""A collections of utilities used by Gwendolyn and her functions""" - -__all__ = ["Options", "Credentials", "databaseFuncs", "EventHandler", "ErrorHandler", "getParams", "logThis", "cap", "makeFiles", "replaceMultiple", "emojiToCommand"] - -from .helperClasses import Options, Credentials, databaseFuncs -from .eventHandlers import EventHandler, ErrorHandler -from .utilFunctions import getParams, logThis, cap, makeFiles, replaceMultiple, emojiToCommand \ No newline at end of file diff --git a/utils/eventHandlers.py b/utils/eventHandlers.py deleted file mode 100644 index 491846b..0000000 --- a/utils/eventHandlers.py +++ /dev/null @@ -1,120 +0,0 @@ -import discord, traceback, discord_slash, sys -from discord.ext import commands - -from .utilFunctions import emojiToCommand - -class EventHandler(): - def __init__(self, bot): - self.bot = bot - - async def on_slash_command(self, ctx): - if ctx.subcommand_name is not None: - subcommand = f" {ctx.subcommand_name} " - else: - subcommand = " " - - if ctx.subcommand_group is not None: - subcommandGroup = f"{ctx.subcommand_group} " - else: - subcommandGroup = "" - - args = " ".join([str(i) for i in ctx.args]) - fullCommand = f"/{ctx.command}{subcommand}{subcommandGroup}{args}" - logMessage = f"{ctx.author.display_name} ran {fullCommand}" - self.bot.log(logMessage, str(ctx.channel_id), level = 25) - - async def on_ready(self): - await self.bot.databaseFuncs.syncCommands() - self.bot.log("Logged in as "+self.bot.user.name+", "+str(self.bot.user.id), level = 25) - game = discord.Game("Use /help for commands") - await self.bot.change_presence(activity=game, status = discord.Status.online) - - async def on_reaction_add(self, reaction : discord.Reaction, user): - if user.bot == False: - message = reaction.message - channel = message.channel - self.bot.log(f"{user.display_name} reacted to a message",str(channel.id)) - try: - connectFourTheirTurn, piece = self.bot.databaseFuncs.connectFourReactionTest(channel,message,"#"+str(user.id)) - except: - connectFourTheirTurn = False - - bedreNetflixMessage, addMovie, imdbIds = self.bot.databaseFuncs.bedreNetflixReactionTest(channel, message) - - if connectFourTheirTurn: - column = emojiToCommand(reaction.emoji) - await self.bot.games.connectFour.placePiece(message, f"#{user.id}", column-1) - elif bedreNetflixMessage and addMovie: - moviePick = emojiToCommand(reaction.emoji) - if moviePick == "none": - imdbID = None - else: - imdbID = imdbIds[moviePick-1] - - if isinstance(channel, discord.DMChannel): - await message.delete() - await self.bot.other.bedreNetflix.addMovie(message, imdbID, False) - else: - await message.clear_reactions() - await self.bot.other.bedreNetflix.addMovie(message, imdbID) - elif bedreNetflixMessage and not addMovie: - showPick = emojiToCommand(reaction.emoji) - if showPick == "none": - imdbName = None - else: - imdbName = imdbIds[showPick-1] - - if isinstance(channel, discord.DMChannel): - await message.delete() - await self.bot.other.bedreNetflix.addShow(message, imdbName, False) - else: - await message.clear_reactions() - await self.bot.other.bedreNetflix.addShow(message, imdbName) - - elif self.bot.databaseFuncs.hangmanReactionTest(channel, message, f"#{user.id}"): - self.bot.log("They reacted to the hangman message") - if ord(reaction.emoji) in range(127462,127488): - guess = chr(ord(reaction.emoji)-127397) - await self.bot.games.hangman.guess(message, f"#{user.id}", guess) - else: - self.bot.log("Bot they didn't react with a valid guess") - -class ErrorHandler(): - def __init__(self, bot): - self.bot = bot - - async def on_slash_command_error(self, ctx, error): - if isinstance(error, commands.CommandNotFound): - await ctx.send("That's not a command (error code 001)") - elif isinstance(error, discord.errors.NotFound): - self.bot.log("Deleted message before I could add all reactions") - elif isinstance(error, commands.errors.MissingRequiredArgument): - self.bot.log(f"{error}",str(ctx.channel_id)) - await ctx.send("Missing command parameters (error code 002). Try using `!help [command]` to find out how to use the command.") - else: - exception = traceback.format_exception(type(error), error, error.__traceback__) - stopAt = "\nThe above exception was the direct cause of the following exception:\n\n" - if stopAt in exception: - index = exception.index(stopAt) - exception = exception[:index] - - exceptionString = "".join(exception) - self.bot.log([f"exception in /{ctx.name}", f"{exceptionString}"],str(ctx.channel_id), 40) - if isinstance(error, discord.errors.NotFound): - self.bot.log("Context is non-existant", level = 40) - else: - await ctx.send("Something went wrong (error code 000)") - - async def on_error(self, method): - errorType = sys.exc_info()[0] - if errorType == discord.errors.NotFound: - self.bot.log("Deleted message before I could add all reactions") - else: - exception = traceback.format_exc() - stopAt = "\nThe above exception was the direct cause of the following exception:\n\n" - if stopAt in exception: - index = exception.index(stopAt) - exception = exception[:index] - - exceptionString = "".join(exception) - self.bot.log([f"exception in {method}", f"{exceptionString}"], level = 40) \ No newline at end of file diff --git a/utils/helperClasses.py b/utils/helperClasses.py deleted file mode 100644 index b643071..0000000 --- a/utils/helperClasses.py +++ /dev/null @@ -1,149 +0,0 @@ -import re, git, os, json, time - -def sanitize(data : str, options : bool = False): - data = data.splitlines() - dct = {} - for line in data: - if line[0] != "#" and ":" in line: - lineValues = line.split(":") - lineValues[0] = lineValues[0].lower() - lineValues[1] = lineValues[1].replace(" ", "") - if options: - lineValues[1] = lineValues[1].lower() - - if lineValues[0] in ["testing guild ids", "admins"]: - lineValues[1] = lineValues[1].split(",") - if all(i.isnumeric() for i in lineValues[1]): - lineValues[1] = [int(i) for i in lineValues[1]] - - if any(i == lineValues[1] for i in ["true", "false"]): - lineValues[1] = (lineValues[1] == "true") - - dct[lineValues[0]] = lineValues[1] - - return dct - -class Options(): - def __init__(self): - with open("options.txt","r") as f: - data = sanitize(f.read(), True) - - self.testing = data["testing"] - self.guildIds = data["testing guild ids"] - self.admins = data["admins"] - -class Credentials(): - def __init__(self): - with open("credentials.txt","r") as f: - data = sanitize(f.read()) - - self.token = data["bot token"] - self.finnhubKey = data["finnhub api key"] - self.wordnikKey = data["wordnik api key"] - self.mongoDBUser = data["mongodb user"] - self.mongoDBPassword = data["mongodb password"] - self.wolfKey = data["wolframalpha appid"] - self.radarrKey = data["radarr api key"] - self.sonarrKey = data["sonarr api key"] - -class databaseFuncs(): - def __init__(self, bot): - self.bot = bot - - def getName(self, userID): - user = self.bot.database["users"].find_one({"_id":userID}) - - if userID == f"#{self.bot.user.id}": - return "Gwendolyn" - elif user != None: - return user["user name"] - else: - self.bot.log(f"Couldn't find user {userID}") - return userID - - def getID(self,userName): - user = self.bot.database["users"].find_one({"user name":re.compile(userName, re.IGNORECASE)}) - - if user != None: - return user["_id"] - else: - self.bot.log("Couldn't find user "+userName) - return None - - def deleteGame(self, gameType, channel): - self.bot.database[gameType].delete_one({"_id":channel}) - - def stopServer(self): - self.bot.database["trivia questions"].delete_many({}) - self.bot.database["blackjack games"].delete_many({}) - self.bot.database["connect 4 games"].delete_many({}) - self.bot.database["hangman games"].delete_many({}) - self.bot.database["hex games"].delete_many({}) - - if not self.bot.options.testing: - g = git.cmd.Git("") - g.pull() - - def connectFourReactionTest(self,channel,message,user): - game = self.bot.database["connect 4 games"].find_one({"_id":str(channel.id)}) - - with open("resources/games/oldImages/connectFour"+str(channel.id), "r") as f: - oldImage = int(f.read()) - - if message.id == oldImage: - self.bot.log("They reacted to the connectFour game") - turn = game["turn"] - if user == game["players"][turn]: - return True, turn+1 - else: - self.bot.log("It wasn't their turn") - return False, 0 - else: - return False, 0 - - def hangmanReactionTest(self, channel, message, user): - try: - with open("resources/games/oldImages/hangman"+str(channel.id), "r") as f: - oldMessages = f.read().splitlines() - except: - return False - gameMessage = False - - for oldMessage in oldMessages: - oldMessageID = int(oldMessage) - if message.id == oldMessageID: - game = self.bot.database["hangman games"].find_one({"_id":str(channel.id)}) - if user == game["player"]: - gameMessage = True - - break - - return gameMessage - - def bedreNetflixReactionTest(self, channel, message): - if os.path.isfile(f"resources/bedreNetflix/oldMessage{str(channel.id)}"): - with open("resources/bedreNetflix/oldMessage"+str(channel.id),"r") as f: - data = json.load(f) - else: - return False, None, None - - if data["messageID"] == message.id: - if "imdbIds" in data: - return True, True, data["imdbIds"] - else: - return True, False, data["imdbNames"] - else: - return False, None, None - - async def syncCommands(self): - collection = self.bot.database["last synced"] - lastSynced = collection.find_one() - now = time.time() - if lastSynced["last synced"] < now - 86400: - slashCommandList = await self.bot.slash.to_dict() - self.bot.log(f"Updating commands: {slashCommandList}") - await self.bot.slash.sync_all_commands() - idNumber = lastSynced["_id"] - queryFilter = {"_id" : idNumber} - update = {"$set" : {"last synced" : now}} - collection.update_one(queryFilter, update) diff --git a/utils/utilFunctions.py b/utils/utilFunctions.py deleted file mode 100644 index 1d0600b..0000000 --- a/utils/utilFunctions.py +++ /dev/null @@ -1,134 +0,0 @@ -import json -import logging -import os -import sys -import imdb -from .helperClasses import Options - -FORMAT = " %(asctime)s | %(name)-16s | %(levelname)-8s | %(message)s" -PRINTFORMAT = "%(asctime)s - %(message)s" -DATEFORMAT = "%Y-%m-%d %H:%M:%S" - -logging.addLevelName(25, "PRINT") -logging.basicConfig(format=FORMAT, datefmt=DATEFORMAT, level=logging.INFO, filename="gwendolyn.log") -logger = logging.getLogger("Gwendolyn") -printer = logging.getLogger("printer") -handler = logging.StreamHandler(sys.stdout) -handler.setFormatter(logging.Formatter(fmt = PRINTFORMAT, datefmt=DATEFORMAT)) -printer.addHandler(handler) -printer.propagate = False - -imdb._logging.setLevel("CRITICAL") - -def getParams(): - with open("resources/slashParameters.json", "r") as f: - params = json.load(f) - - options = Options() - - if options.testing: - for p in params: - params[p]["guild_ids"] = options.guildIds - - return params - -def logThis(messages, channel : str = "", level : int = 20): - channel = channel.replace("Direct Message with ","") - if type(messages) is str: - messages = [messages] - - printMessage = messages[0] - - for x, msg in enumerate(messages): - if channel != "": - messages[x] = f"{msg} - ({channel})" - - if len(messages) > 1: - printMessage += " (details in log)" - - if level >= 25: - printer.log(level, printMessage) - - for logMessage in messages: - logger.log(level, logMessage) - -# Capitalizes all words except some of them -def cap(s): - no_caps_list = ["of","the"] - # Capitalizes a strink like a movie title - word_number = 0 - lst = s.split() - res = '' - for word in lst: - word_number += 1 - if word not in no_caps_list or word_number == 1: - word = word.capitalize() - res += word+" " - res = res[:-1] - return res - -def makeFiles(): - def makeJsonFile(path,content): - # Creates json file if it doesn't exist - if not os.path.isfile(path): - logThis(path.split("/")[-1]+" didn't exist. Making it now.") - with open(path,"w") as f: - json.dump(content,f,indent = 4) - - def makeTxtFile(path,content): - # Creates txt file if it doesn't exist - if not os.path.isfile(path): - logThis(path.split("/")[-1]+" didn't exist. Making it now.") - with open(path,"w") as f: - f.write(content) - - def directory(path): - if not os.path.isdir(path): - os.makedirs(path) - logThis("The "+path.split("/")[-1]+" directory didn't exist") - - with open("resources/startingFiles.json") as f: - data = json.load(f) - - for path in data["folder"]: - directory(path) - - for path, content in data["json"].items(): - makeJsonFile(path,content) - - for path, content in data["txt"].items(): - makeTxtFile(path,content) - -# Replaces multiple things with the same thing -def replaceMultiple(mainString, toBeReplaces, newString): - # Iterate over the strings to be replaced - for elem in toBeReplaces : - # Check if string is in the main string - if elem in mainString : - # Replace the string - mainString = mainString.replace(elem, newString) - - return mainString - -def emojiToCommand(emoji): - if emoji == "1️⃣": - return 1 - elif emoji == "2️⃣": - return 2 - elif emoji == "3️⃣": - return 3 - elif emoji == "4️⃣": - return 4 - elif emoji == "5️⃣": - return 5 - elif emoji == "6️⃣": - return 6 - elif emoji == "7️⃣": - return 7 - elif emoji == "🎲": - return "roll" - elif emoji == "❌": - return "none" - elif emoji == "✔️": - return 1 - else: return ""