From 43f26ec38376ac566a8602e690536000f1f357b5 Mon Sep 17 00:00:00 2001 From: NikolajDanger Date: Thu, 15 Apr 2021 15:16:56 +0200 Subject: [PATCH 01/10] :pencil: Cogs and utils Improved code style and added comments and docstrings to cogs and utils. --- Gwendolyn.py | 137 +++++++++++++++------ cogs/EventCog.py | 20 ++-- cogs/GameCogs.py | 112 ++++++++++------- cogs/LookupCog.py | 18 ++- cogs/MiscCog.py | 53 +++++---- cogs/StarWarsCog.py | 31 ++--- resources/longStrings.json | 4 + utils/__init__.py | 9 +- utils/eventHandlers.py | 184 ++++++++++++++++++---------- utils/helperClasses.py | 238 ++++++++++++++++++++++++++++++++----- utils/utilFunctions.py | 174 ++++++++++++++++++++++----- 11 files changed, 725 insertions(+), 255 deletions(-) create mode 100644 resources/longStrings.json diff --git a/Gwendolyn.py b/Gwendolyn.py index 5a48420..20342c5 100644 --- a/Gwendolyn.py +++ b/Gwendolyn.py @@ -1,63 +1,138 @@ -import os, finnhub, platform, asyncio, discord +""" +Contains the Gwendolyn class, and runs it when run as script. -from discord.ext import commands -from discord_slash import SlashCommand -from pymongo import MongoClient +*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 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 +import discord # Used for discord.Intents and discord.Status +import discord_slash # Used to initialized SlashCommands object + +from discord.ext import commands # Used to inherit from commands.bot +from pymongo import MongoClient # Used for database management from funcs import Money, StarWars, Games, Other, LookupFuncs -from utils import Options, Credentials, logThis, makeFiles, databaseFuncs, EventHandler, ErrorHandler +from utils import (Options, Credentials, logThis, makeFiles, databaseFuncs, + EventHandler, ErrorHandler, longStrings) + 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) + """ + def __init__(self): + """Initialize the bot.""" + intents = discord.Intents.default() + intents.members = True + initParams = { + "command_prefix": " ", + "case_insensitive": True, + "intents": intents, + "status": discord.Status.dnd + } + super().__init__(**initParams) + + self._addClientsAndOptions() + self._addUtilClasses() + self._addFunctionContainers() + self._addCogs() + + def _addClientsAndOptions(self): + """Add all the client, option and credentials objects.""" + self.longStrings = longStrings() 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") + finnhubKey = self.credentials.finnhubKey + self.finnhubClient = finnhub.Client(api_key=finnhubKey) + mongoUser = self.credentials.mongoDBUser + mongoPassword = self.credentials.mongoDBPassword + mongoLink = f"mongodb+srv://{mongoUser}:{mongoPassword}@gwendolyn" + mongoLink += ".qkwfy.mongodb.net/Gwendolyn?retryWrites=true&w=majority" + dataBaseClient = MongoClient(mongoLink) if self.options.testing: self.log("Testing mode") - self.database = self.MongoClient["Gwendolyn-Test"] + self.database = dataBaseClient["Gwendolyn-Test"] else: - self.database = self.MongoClient["Gwendolyn"] + self.database = dataBaseClient["Gwendolyn"] + def _addUtilClasses(self): + """Add all the classes used as utility.""" + self.databaseFuncs = databaseFuncs(self) + self.eventHandler = EventHandler(self) + self.errorHandler = ErrorHandler(self) + slashParams = { + "sync_commands": True, + "sync_on_cog_reload": True, + "override_type": True + } + self.slash = discord_slash.SlashCommand(self, **slashParams) + + def _addFunctionContainers(self): + """Add all the function containers used for commands.""" 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 + def _addCogs(self): + """Load cogs.""" + for filename in os.listdir("./cogs"): + if filename.endswith(".py"): + self.load_extension(f"cogs.{filename[:-3]}") - super().__init__(command_prefix=" ", case_insensitive=True, intents = intents, status = discord.Status.dnd) - - def log(self, messages, channel : str = "", level : int = 20): + def log(self, messages, channel: str = "", level: int = 20): + """Log a message. Described in utils/utilFunctions.py.""" logThis(messages, channel, level) - async def stop(self, ctx): + 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) + await self.change_presence(status=discord.Status.offline) - self.databaseFuncs.stopServer() + self.databaseFuncs.wipeGames() - self.log("Logging out", level = 25) + 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)") + logMessage = f"{ctx.author.display_name} tried to stop me!" + self.log(logMessage, str(ctx.channel_id)) + await ctx.send(f"I don't think I will, {ctx.author.display_name}") - async def defer(self, ctx): + async def defer(self, ctx: discord_slash.context.SlashContext): + """Send a "Gwendolyn is thinking" message to the user.""" try: await ctx.defer() - except: + except discord_slash.error.AlreadyResponded: self.log("defer failed") - if __name__ == "__main__": if platform.system() == "Windows": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) @@ -67,15 +142,9 @@ if __name__ == "__main__": # 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 + except Exception: + bot.log(bot.longStrings["Can't log in"]) diff --git a/cogs/EventCog.py b/cogs/EventCog.py index 7b72f6b..50fc3b9 100644 --- a/cogs/EventCog.py +++ b/cogs/EventCog.py @@ -1,34 +1,40 @@ -from discord.ext import commands +"""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 - # Syncs commands, sets the game, and logs when the bot logs in @commands.Cog.listener() async def on_ready(self): + """Log and set bot status when bot logs in.""" await self.bot.eventHandler.on_ready() - # Logs when user sends a command @commands.Cog.listener() async def on_slash_command(self, ctx): + """Log when a slash command is run.""" 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): + """Log when a slash error occurs.""" await self.bot.errorHandler.on_slash_command_error(ctx, error) - # Logs if on error occurs async def on_error(self, method, *args, **kwargs): + """Log when an error occurs.""" 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): + """Handle when someone reacts to a message.""" await self.bot.eventHandler.on_reaction_add(reaction, user) + def setup(bot): + """Add the eventcog to the bot.""" bot.add_cog(EventCog(bot)) diff --git a/cogs/GameCogs.py b/cogs/GameCogs.py index 4714437..c813e24 100644 --- a/cogs/GameCogs.py +++ b/cogs/GameCogs.py @@ -1,155 +1,177 @@ -from discord.ext import commands -from discord_slash import cog_ext +"""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 utils import getParams +from utils import getParams # pylint: disable=import-error params = getParams() + class GamesCog(commands.Cog): - def __init__(self,bot): - """Runs game stuff.""" + """Contains miscellaneous game commands.""" + + def __init__(self, bot): + """Initialize the cog.""" self.bot = bot - # Checks user balance @cog_ext.cog_slash(**params["balance"]) async def balance(self, ctx): + """Check user balance.""" 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): + """Give another user an amount of GwendoBucks.""" 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"): + async def invest(self, ctx, parameters="check"): + """Invest GwendoBucks in the stock market.""" 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 = ""): + async def trivia(self, ctx, answer=""): + """Run a game of trivia.""" await self.bot.games.trivia.triviaParse(ctx, answer) class BlackjackCog(commands.Cog): - def __init__(self,bot): - """Runs game stuff.""" + """Contains the blackjack commands.""" + + def __init__(self, bot): + """Initialize the cog.""" self.bot = bot - # Starts a game of blackjack @cog_ext.cog_subcommand(**params["blackjackStart"]) async def blackjackStart(self, ctx): + """Start a game of blackjack.""" await self.bot.games.blackjack.start(ctx) @cog_ext.cog_subcommand(**params["blackjackBet"]) async def blackjackBet(self, ctx, bet): + """Enter the game of blackjack with a bet.""" await self.bot.games.blackjack.playerDrawHand(ctx, bet) @cog_ext.cog_subcommand(**params["blackjackStand"]) - async def blackjackStand(self, ctx, hand = ""): + async def blackjackStand(self, ctx, hand=""): + """Stand on your hand in blackjack.""" await self.bot.games.blackjack.stand(ctx, hand) @cog_ext.cog_subcommand(**params["blackjackHit"]) - async def blackjackHit(self, ctx, hand = 0): + async def blackjackHit(self, ctx, hand=0): + """Hit on your hand in blackjack.""" await self.bot.games.blackjack.hit(ctx, hand) @cog_ext.cog_subcommand(**params["blackjackDouble"]) - async def blackjackDouble(self, ctx, hand = 0): + async def blackjackDouble(self, ctx, hand=0): + """Double in blackjack.""" await self.bot.games.blackjack.double(ctx, hand) @cog_ext.cog_subcommand(**params["blackjackSplit"]) - async def blackjackSplit(self, ctx, hand = 0): + async def blackjackSplit(self, ctx, hand=0): + """Split your hand in blackjack.""" await self.bot.games.blackjack.split(ctx, hand) @cog_ext.cog_subcommand(**params["blackjackHilo"]) async def blackjackHilo(self, ctx): + """Get the hilo value for the deck in blackjack.""" await self.bot.games.blackjack.hilo(ctx) @cog_ext.cog_subcommand(**params["blackjackShuffle"]) async def blackjackShuffle(self, ctx): + """Shuffle the blackjack game.""" await self.bot.games.blackjack.shuffle(ctx) @cog_ext.cog_subcommand(**params["blackjackCards"]) async def blackjackCards(self, ctx): + """Get the amount of cards left in the blackjack deck.""" await self.bot.games.blackjack.cards(ctx) class ConnectFourCog(commands.Cog): - def __init__(self,bot): - """Runs game stuff.""" + """Contains all the connect four commands.""" + + def __init__(self, bot): + """Initialize the cog.""" self.bot = bot - # Start a game of connect four against a user @cog_ext.cog_subcommand(**params["connectFourStartUser"]) async def connectFourStartUser(self, ctx, user): + """Start a game of connect four against another 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): + async def connectFourStartGwendolyn(self, ctx, difficulty=3): + """Start a game of connect four against Gwendolyn.""" 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): + """Surrender the game of connect four.""" await self.bot.games.connectFour.surrender(ctx) class HangmanCog(commands.Cog): - def __init__(self,bot): - """Runs game stuff.""" + """Contains all the hangman commands.""" + + def __init__(self, bot): + """Initialize the cog.""" self.bot = bot - # Starts a game of Hangman @cog_ext.cog_subcommand(**params["hangmanStart"]) async def hangmanStart(self, ctx): + """Start a game of hangman.""" await self.bot.games.hangman.start(ctx) - # Stops a game of Hangman @cog_ext.cog_subcommand(**params["hangmanStop"]) async def hangmanStop(self, ctx): + """Stop the current game of hangman.""" await self.bot.games.hangman.stop(ctx) class HexCog(commands.Cog): - def __init__(self,bot): - """Runs game stuff.""" - self.bot = bot + """Contains all the hex commands.""" + + def __init__(self, bot): + """Initialize the cog.""" + self.bot = bot + self.hex = self.bot.games.hex - # 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 another player.""" + await self.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) + async def hexStartGwendolyn(self, ctx, difficulty=2): + """Start a game of hex against Gwendolyn.""" + await self.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}") + """Place a piece in the hex game.""" + await self.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) + """Undo your last hex move.""" + await self.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) + """Perform a hex swap.""" + await self.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) + """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)) \ No newline at end of file + bot.add_cog(HexCog(bot)) diff --git a/cogs/LookupCog.py b/cogs/LookupCog.py index 1b23cd1..f41d38b 100644 --- a/cogs/LookupCog.py +++ b/cogs/LookupCog.py @@ -1,24 +1,32 @@ -from discord.ext import commands -from discord_slash import cog_ext +"""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 utils import getParams +from utils import getParams # pylint: disable=import-error params = getParams() + class LookupCog(commands.Cog): + """Contains the lookup commands.""" + def __init__(self, bot): - """Runs lookup commands.""" + """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.lookupFuncs.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.lookupFuncs.monsterFunc(ctx, query) + def setup(bot): - bot.add_cog(LookupCog(bot)) \ No newline at end of file + """Add the cog to the bot.""" + bot.add_cog(LookupCog(bot)) diff --git a/cogs/MiscCog.py b/cogs/MiscCog.py index 136c0fb..ab6e146 100644 --- a/cogs/MiscCog.py +++ b/cogs/MiscCog.py @@ -1,94 +1,99 @@ -import discord, codecs, string, json -from discord.ext import commands -from discord_slash import cog_ext +"""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 utils import getParams # pylint: disable=import-error +from utils import getParams # pylint: disable=import-error params = getParams() + class MiscCog(commands.Cog): + """Contains the miscellaneous commands.""" + def __init__(self, bot): - """Runs misc commands.""" + """Initialize the cog.""" 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): + """Send the bot's latency.""" 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): + """Stop the bot.""" await self.bot.stop(ctx) - # Gets help for specific command @cog_ext.cog_slash(**params["help"]) - async def helpCommand(self, ctx, command = ""): + async def helpCommand(self, ctx, command=""): + """Get help for commands.""" await self.bot.other.helpFunc(ctx, command) - # Lets you thank the bot @cog_ext.cog_slash(**params["thank"]) async def thank(self, ctx): + """Thank the bot.""" await ctx.send("You're welcome :blush:") - # Sends a friendly message @cog_ext.cog_slash(**params["hello"]) async def hello(self, ctx): + """Greet the bot.""" await self.bot.other.helloFunc(ctx) - # Rolls dice @cog_ext.cog_slash(**params["roll"]) - async def roll(self, ctx, dice = "1d20"): + async def roll(self, ctx, dice="1d20"): + """Roll dice.""" await self.bot.other.rollDice(ctx, dice) - # Sends a random image @cog_ext.cog_slash(**params["image"]) async def image(self, ctx): + """Get a random image from Bing.""" await self.bot.other.imageFunc(ctx) - # Finds a random movie @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) - # Generates a random name @cog_ext.cog_slash(**params["name"]) async def name(self, ctx): + """Generate a random name.""" await self.generators.nameGen(ctx) - # Generates a random tavern name @cog_ext.cog_slash(**params["tavern"]) async def tavern(self, ctx): + """Generate a random tavern name.""" await self.generators.tavernGen(ctx) - # Finds a page on the Senkulpa wiki @cog_ext.cog_slash(**params["wiki"]) - async def wiki(self, ctx, wikiPage = ""): + async def wiki(self, ctx, wikiPage=""): + """Get a page on a fandom wiki.""" 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): + """Search for a movie and add it to the Plex server.""" 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): + """Search for a show and add it to the Plex server.""" await self.bedreNetflix.requestShow(ctx, show) - #Returns currently downloading torrents @cog_ext.cog_slash(**params["downloading"]) - async def downloading(self, ctx, parameters = "-d"): + async def downloading(self, ctx, parameters="-d"): + """Get the current downloading torrents.""" await self.bedreNetflix.downloading(ctx, parameters) - #Looks up on Wolfram Alpha @cog_ext.cog_slash(**params["wolf"]) async def wolf(self, ctx, query): + """Perform a search on Wolfram Alpha.""" await self.nerdShit.wolfSearch(ctx, query) + def setup(bot): + """Add the cog to the bot.""" bot.add_cog(MiscCog(bot)) diff --git a/cogs/StarWarsCog.py b/cogs/StarWarsCog.py index 57e39f0..72516d4 100644 --- a/cogs/StarWarsCog.py +++ b/cogs/StarWarsCog.py @@ -1,37 +1,40 @@ -import discord, string, json +"""Contains the StarWarsCog, which deals with Star Wars commands.""" from discord.ext import commands from discord_slash import cog_ext -from utils import getParams +from utils import getParams # pylint: disable=import-error params = getParams() -class starWarsCog(commands.Cog): + +class StarWarsCog(commands.Cog): + """Contains the Star Wars commands.""" def __init__(self, bot): - """Runs star wars commands.""" + """Initialize the cog.""" self.bot = bot - # Rolls star wars dice @cog_ext.cog_slash(**params["starWarsRoll"]) - async def starWarsRoll(self, ctx, dice = ""): + async def starWarsRoll(self, ctx, dice=""): + """Roll Star Wars dice.""" await self.bot.starWars.roll.parseRoll(ctx, dice) - # Controls destiny points @cog_ext.cog_slash(**params["starWarsDestiny"]) - async def starWarsDestiny(self, ctx, parameters = ""): + async def starWarsDestiny(self, ctx, parameters=""): + """Control Star Wars destiny points.""" 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): + async def starWarsCrit(self, ctx, severity: int = 0): + """Roll for critical injuries.""" 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 = ""): + async def starWarsCharacter(self, ctx, parameters=""): + """Access and change Star Wars character sheet data.""" await self.bot.starWars.character.parseChar(ctx, parameters) + def setup(bot): - bot.add_cog(starWarsCog(bot)) \ No newline at end of file + """Add the cog to the bot.""" + bot.add_cog(StarWarsCog(bot)) diff --git a/resources/longStrings.json b/resources/longStrings.json new file mode 100644 index 0000000..a7b418f --- /dev/null +++ b/resources/longStrings.json @@ -0,0 +1,4 @@ +{ + "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" +} \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py index 7d2a46c..43fa506 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -1,7 +1,10 @@ -"""A collections of utilities used by Gwendolyn and her functions""" +"""A collections of utilities used by Gwendolyn and her functions.""" -__all__ = ["Options", "Credentials", "databaseFuncs", "EventHandler", "ErrorHandler", "getParams", "logThis", "cap", "makeFiles", "replaceMultiple", "emojiToCommand"] +__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 +from .utilFunctions import (getParams, logThis, cap, makeFiles, + replaceMultiple, emojiToCommand, longStrings) diff --git a/utils/eventHandlers.py b/utils/eventHandlers.py index 491846b..1ec9810 100644 --- a/utils/eventHandlers.py +++ b/utils/eventHandlers.py @@ -1,13 +1,52 @@ -import discord, traceback, discord_slash, sys -from discord.ext import commands +""" +Classes used to handle bot events and errors. + +*Classes* +--------- + EventHandler + ErrorHandler +""" +import discord # Used to init discord.Game and discord.Status, as well +# as compare errors to discord errors and as typehints +import traceback # Used to get the traceback of errors +import sys # Used to get traceback when the specific error is not +# available +from discord.ext import commands # Used to compare errors with command +# errors + +from discord_slash.context import SlashContext +from utils.utilFunctions import emojiToCommand -from .utilFunctions import emojiToCommand 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_slash_command(self, ctx): + async def on_ready(self): + """Log and sets status when it logs in.""" + await self.bot.databaseFuncs.syncCommands() + name = self.bot.user.name + userid = str(self.bot.user.id) + loggedInMessage = f"Logged in as {name}, {userid}" + self.bot.log(loggedInMessage, level=25) + game = discord.Game("Use /help for commands") + + onlineStatus = discord.Status.online + await self.bot.change_presence(activity=game, status=onlineStatus) + + 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: @@ -21,100 +60,117 @@ class EventHandler(): 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) + 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: + 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.databaseFuncs 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 + reactedMessage = f"{user.display_name} reacted to a message" + self.bot.log(reactedMessage, str(channel.id)) + plexData = tests.bedreNetflixReactionTest(message) + # plexData is a list containing 3 elements: whether it was + # the addshow/addmovie 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). - bedreNetflixMessage, addMovie, imdbIds = self.bot.databaseFuncs.bedreNetflixReactionTest(channel, message) + reactionTestParams = [message, f"#{str(user.id)}"] - if connectFourTheirTurn: + if tests.connectFourReactionTest(*reactionTestParams): 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] + params = [message, f"#{user.id}", column-1] + await self.bot.games.connectFour.placePiece(*params) - 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 plexData[0]: + plexFuncs = self.bot.other.bedreNetflix + if plexData[1]: + moviePick = emojiToCommand(reaction.emoji) + if moviePick == "none": + imdbID = None + else: + imdbID = plexData[2][moviePick-1] - if isinstance(channel, discord.DMChannel): - await message.delete() - await self.bot.other.bedreNetflix.addShow(message, imdbName, False) + if isinstance(channel, discord.DMChannel): + await message.delete() + await plexFuncs.addMovie(message, imdbID, False) + else: + await message.clear_reactions() + await plexFuncs.addMovie(message, imdbID) else: - await message.clear_reactions() - await self.bot.other.bedreNetflix.addShow(message, imdbName) + showPick = emojiToCommand(reaction.emoji) + if showPick == "none": + imdbName = None + else: + imdbName = plexData[2][showPick-1] - elif self.bot.databaseFuncs.hangmanReactionTest(channel, message, f"#{user.id}"): + if isinstance(channel, discord.DMChannel): + await message.delete() + await plexFuncs.addShow(message, imdbName, False) + else: + await message.clear_reactions() + await plexFuncs.addShow(message, imdbName) + + elif tests.hangmanReactionTest(*reactionTestParams): self.bot.log("They reacted to the hangman message") - if ord(reaction.emoji) in range(127462,127488): + if ord(reaction.emoji) in range(127462, 127488): + # The range is letter-emojis guess = chr(ord(reaction.emoji)-127397) - await self.bot.games.hangman.guess(message, f"#{user.id}", guess) + # 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, error): + 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 (error code 001)") + 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("Missing command parameters (error code 002). Try using `!help [command]` to find out how to use the command.") + self.bot.log(f"{error}", str(ctx.channel_id)) + await ctx.send(self.bot.longStrings["missing parameters"]) 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] + params = [type(error), error, error.__traceback__] + exception = traceback.format_exception(*params) exceptionString = "".join(exception) - self.bot.log([f"exception in /{ctx.name}", f"{exceptionString}"],str(ctx.channel_id), 40) + logMessages = [f"exception in /{ctx.name}", f"{exceptionString}"] + self.bot.log(logMessages, str(ctx.channel_id), 40) if isinstance(error, discord.errors.NotFound): - self.bot.log("Context is non-existant", level = 40) + 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): + async def on_error(self, method: str): + """Log when there's an error.""" 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 + logMessages = [f"exception in {method}", f"{exceptionString}"] + self.bot.log(logMessages, level=40) diff --git a/utils/helperClasses.py b/utils/helperClasses.py index 53894f3..84b1019 100644 --- a/utils/helperClasses.py +++ b/utils/helperClasses.py @@ -1,6 +1,44 @@ -import re, git, os, json, time +""" +Contains classes used for utilities. -def sanitize(data : str, options : bool = False): +*Functions* +----------- + Sanitize(data: str, lowerCaseValue: bool = false) -> dict + +*Classes* +--------- + Options() + Credentials() + DatabaseFuncs() +""" +import re # Used in getID +import git # Used to pull when stopping +import os # Used to test if files exist +import json # Used to read the data about addmovie/addshow +import time # Used to test how long it's been since commands were synced +import discord # Used for type hints + + +def sanitize(data: str, lowerCaseValue: 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. + lowerCaseValue: 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: @@ -8,7 +46,7 @@ def sanitize(data : str, options : bool = False): lineValues = line.split(":") lineValues[0] = lineValues[0].lower() lineValues[1] = lineValues[1].replace(" ", "") - if options: + if lowerCaseValue: lineValues[1] = lineValues[1].lower() if lineValues[0] in ["testing guild ids", "admins"]: @@ -23,18 +61,26 @@ def sanitize(data : str, options : bool = False): return dct + class Options(): + """Contains the options for the bot.""" + def __init__(self): - with open("options.txt","r") as f: + """Initialize the options.""" + 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(): + """Contains the credentials for the bot and apis.""" + def __init__(self): - with open("credentials.txt","r") as f: + """Initialize the credentials.""" + with open("credentials.txt", "r") as f: data = sanitize(f.read()) self.token = data["bot token"] @@ -46,34 +92,99 @@ class Credentials(): self.radarrKey = data["radarr api key"] self.sonarrKey = data["sonarr api key"] + class databaseFuncs(): + """ + Manages database functions. + + *Methods* + --------- + getName(userID: str) -> str + getID(userName: str) -> str + deleteGame(gameType: str, channel: str) + wipeGames() + connectFourReactionTest(message: discord.Message, + user: discord.User) -> bool + hangmanReactionTest(message: discord.Message, + user: discord.User) -> bool + BedreNetflixReactionTest(message: discord.Message, + user: discord.User) -> bool, bool, + list + syncCommands() + """ + def __init__(self, bot): + """Initialize the class.""" self.bot = bot - def getName(self, userID): - user = self.bot.database["users"].find_one({"_id":userID}) + def getName(self, userID: str): + """ + Get the name of a user you have the # id of. + + *Parameters: + ------------ + userID: str + The id of the user you want the name of. The format is + "#" + str(discord.User.id) + + *Returns* + --------- + userName: str + The name of the user. If the user couldn't be found, + returns the userID. + """ + user = self.bot.database["users"].find_one({"_id": userID}) if userID == f"#{self.bot.user.id}": return "Gwendolyn" - elif user != None: + elif user is not 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)}) + def getID(self, userName: str): + """ + Get the id of a user you have the username of. - if user != None: + *Parameters: + ------------ + userName: str + The name of the user you want the id of. + + *Returns* + --------- + userID: str + The id of the user in the format "#" + + str(discord.User.id). If the user couldn't be found, + returns the userName. + """ + userSearch = {"user name": re.compile(userName, re.IGNORECASE)} + user = self.bot.database["users"].find_one(userSearch) + + if user is not 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 deleteGame(self, gameType: str, channel: str): + """ + Remove a game from the database. - def stopServer(self): + *Parameters* + ------------ + gameType: 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[gameType].delete_one({"_id": channel}) + + def wipeGames(self): + """Delete all running games and pull from git.""" self.bot.database["trivia questions"].delete_many({}) self.bot.database["blackjack games"].delete_many({}) self.bot.database["connect 4 games"].delete_many({}) @@ -84,35 +195,80 @@ class databaseFuncs(): 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)}) + def connectFourReactionTest(self, message: discord.Message, + user: discord.User): + """ + Test if the given message is the current connect four game. - with open("resources/games/oldImages/connectFour"+str(channel.id), "r") as f: + 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 + channelSearch = {"_id": str(channel.id)} + game = self.bot.database["connect 4 games"].find_one(channelSearch) + + filePath = f"resources/games/oldImages/connectFour{channel.id}" + with open(filePath, "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 + return True else: self.bot.log("It wasn't their turn") - return False, 0 + return False else: - return False, 0 + return False - def hangmanReactionTest(self, channel, message, user): - try: - with open("resources/games/oldImages/hangman"+str(channel.id), "r") as f: + def hangmanReactionTest(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 + filePath = f"resources/games/oldImages/hangman{channel.id}" + if os.path.isfile(filePath): + with open(filePath, "r") as f: oldMessages = f.read().splitlines() - except: + else: 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)}) + database = self.bot.database["hangman games"] + channelSearch = {"_id": str(channel.id)} + game = database.find_one(channelSearch) if user == game["player"]: gameMessage = True @@ -120,9 +276,30 @@ class databaseFuncs(): 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: + def bedreNetflixReactionTest(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 + filePath = f"resources/bedreNetflix/oldMessage{str(channel.id)}" + if os.path.isfile(filePath): + with open(filePath, "r") as f: data = json.load(f) else: return False, None, None @@ -136,6 +313,7 @@ class databaseFuncs(): return False, None, None async def syncCommands(self): + """Sync the slash commands with the discord API.""" collection = self.bot.database["last synced"] lastSynced = collection.find_one() now = time.time() @@ -144,6 +322,6 @@ class databaseFuncs(): 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}} + queryFilter = {"_id": idNumber} + update = {"$set": {"last synced": now}} collection.update_one(queryFilter, update) diff --git a/utils/utilFunctions.py b/utils/utilFunctions.py index 92c655a..6fea627 100644 --- a/utils/utilFunctions.py +++ b/utils/utilFunctions.py @@ -1,3 +1,18 @@ +""" +Contains utility functions used by parts of the bot. + +*Functions* +----------- + longstrings() -> dict + getParams() -> dict + logThis(messages: Union[str, list], channel: str = "", + level: int = 20) + cap(s: str) -> str + makeFiles() + replaceMultiple(mainString: str, toBeReplaced: list, + newString: str) -> str + emojiToCommand(emoji: str) -> str +""" import json import logging import os @@ -5,22 +20,55 @@ import sys import imdb from .helperClasses import Options + +# 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") -logging.basicConfig(format=FORMAT, datefmt=DATEFORMAT, level=logging.INFO, filename="gwendolyn.log") +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)) +handler.setFormatter(logging.Formatter(fmt=PRINTFORMAT, datefmt=DATEFORMAT)) printer.addHandler(handler) printer.propagate = False -imdb._logging.setLevel("CRITICAL") +imdb._logging.setLevel("CRITICAL") # Basically disables imdbpy +# logging, since it's printed to the terminal. + + +def longStrings(): + """ + Get the data from resources/longStrings.json. + + *Returns* + --------- + data: dict + The long strings and their keys. + """ + with open("resources/longStrings.json", "r") as f: + data = json.load(f) + + return data + def getParams(): + """ + Get the slash command parameters. + + *Returns* + --------- + params: dict + The parameters for every slash command. + """ with open("resources/slashParameters.json", "r") as f: params = json.load(f) @@ -32,8 +80,25 @@ def getParams(): return params -def logThis(messages, channel : str = "", level : int = 20): - channel = channel.replace("Direct Message with ","") + +def logThis(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 type(messages) is str: messages = [messages] @@ -41,9 +106,11 @@ def logThis(messages, channel : str = "", level : int = 20): for x, msg in enumerate(messages): if channel != "": - messages[x] = f"{msg} - ({channel})" + messages[x] = f"{msg} - ({channel})" # Adds channel to log + # messages - if len(messages) > 1: + if len(messages) > 1: # Tells user to check the log if there are + # more messages there printMessage += " (details in log)" if level >= 25: @@ -52,10 +119,24 @@ def logThis(messages, channel : str = "", level : int = 20): 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 + +def cap(s: str): + """ + Capitalize a string like a movie title. + + That means "of" and "the" are not capitalized. + + *Parameters* + ------------ + s: str + The string to capitalized. + + *Returns* + --------- + res: str + The capitalized string. + """ + no_caps_list = ["of", "the"] word_number = 0 lst = s.split() res = '' @@ -67,22 +148,25 @@ def cap(s): 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 +def makeFiles(): + """Create all the files and directories needed by Gwendolyn.""" + def makeJsonFile(path, content): + """Create 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: + with open(path, "w") as f: + json.dump(content, f, indent=4) + + def makeTxtFile(path, content): + """Create 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): + """Create directory if it doesn't exist.""" if not os.path.isdir(path): os.makedirs(path) logThis("The "+path.split("/")[-1]+" directory didn't exist") @@ -91,26 +175,57 @@ def makeFiles(): data = json.load(f) for path, content in data["json"].items(): - makeJsonFile(path,content) + makeJsonFile(path, content) for path, content in data["txt"].items(): - makeTxtFile(path,content) + makeTxtFile(path, content) for path in data["folder"]: directory(path) -# Replaces multiple things with the same thing -def replaceMultiple(mainString, toBeReplaces, newString): + +def replaceMultiple(mainString: str, toBeReplaced: list, newString: str): + """ + Replace multiple substrings in a string with the same substring. + + *Parameters* + ------------ + mainString: str + The string to replace substrings in. + toBeReplaced: list + The substrings to replace. + newString: str + The string to replace the substrings with. + + *Returns* + --------- + mainString: str + The string with the substrings replaced. + """ # Iterate over the strings to be replaced - for elem in toBeReplaces : + for elem in toBeReplaced: # Check if string is in the main string - if elem in mainString : + if elem in mainString: # Replace the string mainString = mainString.replace(elem, newString) return mainString -def emojiToCommand(emoji): + +def emojiToCommand(emoji: str): + """ + Convert emoji to text. + + *Parameters* + ------------ + emoji: str + The emoji to decipher. + + *Returns* + --------- + : str + The deciphered string. + """ if emoji == "1️⃣": return 1 elif emoji == "2️⃣": @@ -131,4 +246,5 @@ def emojiToCommand(emoji): return "none" elif emoji == "✔️": return 1 - else: return "" + else: + return "" From de127a179b5cf0f92197462d0b932183c1748eb5 Mon Sep 17 00:00:00 2001 From: NikolajDanger Date: Sat, 17 Apr 2021 20:59:47 +0200 Subject: [PATCH 02/10] :black_joker: Blackjack --- cogs/GameCogs.py | 2 +- funcs/__init__.py | 2 +- funcs/games/blackjack.py | 1489 ++++++++++++++++++++++++------------ requirements.txt | 6 +- resources/longStrings.json | 10 +- utils/utilFunctions.py | 12 +- 6 files changed, 1016 insertions(+), 505 deletions(-) diff --git a/cogs/GameCogs.py b/cogs/GameCogs.py index c813e24..ebd1f54 100644 --- a/cogs/GameCogs.py +++ b/cogs/GameCogs.py @@ -50,7 +50,7 @@ class BlackjackCog(commands.Cog): @cog_ext.cog_subcommand(**params["blackjackBet"]) async def blackjackBet(self, ctx, bet): """Enter the game of blackjack with a bet.""" - await self.bot.games.blackjack.playerDrawHand(ctx, bet) + await self.bot.games.blackjack.enterGame(ctx, bet) @cog_ext.cog_subcommand(**params["blackjackStand"]) async def blackjackStand(self, ctx, hand=""): diff --git a/funcs/__init__.py b/funcs/__init__.py index 729f445..597efd8 100644 --- a/funcs/__init__.py +++ b/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 diff --git a/funcs/games/blackjack.py b/funcs/games/blackjack.py index 1d68b0d..075e33d 100644 --- a/funcs/games/blackjack.py +++ b/funcs/games/blackjack.py @@ -1,47 +1,109 @@ -import random -import math -import datetime -import asyncio -import discord +""" +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 +import discord # Used for discord.file +import discord_slash # Used for typehints from PIL import Image, ImageDraw, ImageFont from shutil import copyfile from utils import replaceMultiple + class Blackjack(): - def __init__(self,bot): + """ + Deals with blackjack commands and gameplay logic. + + *Methods* + --------- + hit(ctx: discord_slash.context.SlashContext, + handNumber: int = 0) + double(ctx: discord_slash.context.SlashContext, + handNumber: int = 0) + stand(ctx: discord_slash.context.SlashContext, + handNumber: int = 0) + split(ctx: discord_slash.context.SlashContext, + handNumber: int = 0) + enterGame(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 - # Shuffles the blackjack cards - def blackjackShuffle(self, decks, channel): + def _blackjackShuffle(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") - with open("resources/games/deckOfCards.txt","r") as f: + with open("resources/games/deckOfCards.txt", "r") as f: deck = f.read() - allDecks = deck.split("\n") * decks + allDecks = deck.split("\n") * self.decks random.shuffle(allDecks) - self.bot.database["blackjack cards"].update_one({"_id":channel},{"$set":{"_id":channel,"cards":allDecks}},upsert=True) + blackjackCards = self.bot.database["blackjack cards"] + cards = {"_id": channel} + cardUpdater = {"$set": {"_id": channel, "cards": allDecks}} + blackjackCards.update_one(cards, cardUpdater, upsert=True) # Creates hilo file - self.bot.log("creating hilo doc for "+channel) + self.bot.log(f"creating hilo doc for {channel}") data = 0 - self.bot.database["hilo"].update_one({"_id":channel},{"$set":{"_id":channel,"hilo":data}},upsert=True) + blackjackHilo = self.bot.database["hilo"] + hiloUpdater = {"$set": {"_id": channel, "hilo": data}} + blackjackHilo.update_one({"_id": channel}, hiloUpdater, upsert=True) - return + def _calcHandValue(self, hand: list): + """ + Calculate the value of a blackjack hand. - # Calculates the value of a blackjack hand - def calcHandValue(self, hand : list): - self.bot.log("Calculating hand value") + *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* + --------- + handValue: int + The blackjack value of the hand. + """ values = [] values.append(0) for card in hand: cardValue = card[0] - cardValue = replaceMultiple(cardValue,["0","k","q","j"],"10") + cardValue = replaceMultiple(cardValue, ["0", "k", "q", "j"], "10") if cardValue == "a": length = len(values) for x in range(length): @@ -58,102 +120,180 @@ class Blackjack(): if value <= 21: handValue = value - self.bot.log("Calculated "+str(hand)+" to be "+str(handValue)) + self.bot.log(f"Calculated the value of {hand} to be {handValue}") return handValue - # Draws a card from the deck - def drawCard(self, channel): + def _drawCard(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") - 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]) + blackjackCards = self.bot.database["blackjack cards"] + drawnCard = blackjackCards.find_one({"_id": channel})["cards"][0] + blackjackCards.update_one({"_id": channel}, {"$pop": {"cards": -1}}) + value = self._calcHandValue([drawnCard]) + + blackjackHilo = self.bot.database["hilo"] if value <= 6: - self.bot.database["hilo"].update_one({"_id":channel},{"$inc":{"hilo":1}}) + blackjackHilo.update_one({"_id": channel}, {"$inc": {"hilo": 1}}) elif value >= 10: - self.bot.database["hilo"].update_one({"_id":channel},{"$inc":{"hilo":-1}}) + blackjackHilo.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}) + def _dealerDraw(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 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}}) + blackjackGames = self.bot.database["blackjack games"] + + if self._calcHandValue(dealerHand) < 17: + dealerHand.append(self._drawCard(channel)) + dealerUpdater = {"$set": {"dealer hand": dealerHand}} + blackjackGames.update_one({"_id": channel}, dealerUpdater) else: done = True - if self.calcHandValue(dealerHand) > 21: - self.bot.database["blackjack games"].update_one({"_id":channel},{"$set":{"dealer busted":True}}) + if self._calcHandValue(dealerHand) > 21: + dealerUpdater = {"$set": {"dealer busted": True}} + blackjackGames.update_one({"_id": channel}, dealerUpdater) return done - # Goes to the next round and calculates some stuff - def blackjackContinue(self, channel): + def _blackjackContinue(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* + --------- + sendMessage: str + The message to send to the channel. + allStanding: 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}) + game = self.bot.database["blackjack games"].find_one({"_id": channel}) done = False - self.bot.database["blackjack games"].update_one({"_id":channel},{"$inc":{"round":1}}) + blackjackGames = self.bot.database["blackjack games"] + blackjackGames.update_one({"_id": channel}, {"$inc": {"round": 1}}) allStanding = True preAllStanding = True - message = "All players are standing. The dealer now shows his cards and draws." + message = self.bot.longStrings["Blackjack all players standing"] if game["all standing"]: self.bot.log("All are standing") - done = self.dealerDraw(channel) + done = self._dealerDraw(channel) message = "The dealer draws a card." - game = self.bot.database["blackjack games"].find_one({"_id":channel}) + blackjackGames.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)") + userHand = game["user hands"][user] + testParams = [userHand, allStanding, preAllStanding, True] + standingTest = (self._testIfStanding(*testParams)) + newUser, allStanding, preAllStanding = standingTest + handUpdater = {"$set": {"user hands."+user: newUser}} + blackjackGames.update_one({"_id": channel}, handUpdater) if allStanding: - self.bot.database["blackjack games"].update_one({"_id":channel},{"$set":{"all standing":True}}) + gameUpdater = {"$set": {"all standing": True}} + blackjackGames.update_one({"_id": channel}, gameUpdater) - try: - self.draw.drawImage(channel) - except: - self.bot.log("Error drawing blackjack table (error code 1340)") + self.draw.drawImage(channel) if allStanding: - if done == False: + if not done: 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\"" + if game["round"] == 0: + firstRoundMsg = self.bot.longStrings["Blackjack first round"] 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 + firstRoundMsg = "" - def testIfStanding(self, hand,allStanding,preAllStanding,topLevel): - if hand["hit"] == False: + sendMessage = self.bot.longStrings["Blackjack commands"] + print(firstRoundMsg) + sendMessage = sendMessage.format(firstRoundMsg) + return sendMessage, False, done + + def _testIfStanding(self, hand: dict, allStanding: bool, + preAllStanding: bool, topLevel: 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. + allStanding: 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. + preAllStanding: bool + Is set to True at the top level. + topLevel: 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. + allStanding: bool + If the player is standing on all their hands. + preAllStanding: bool + Is true if allStanding 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 hand["standing"] == False: + if not hand["standing"]: allStanding = False - if self.calcHandValue(hand["hand"]) >= 21 or hand["doubled"]: + if self._calcHandValue(hand["hand"]) >= 21 or hand["doubled"]: hand["standing"] = True else: preAllStanding = False @@ -162,29 +302,344 @@ class Blackjack(): if topLevel: if hand["split"] >= 1: - hand["other hand"], allStanding, preAllStanding = self.testIfStanding(hand["other hand"],allStanding,preAllStanding,False) + testHand = hand["other hand"] + testParams = [testHand, allStanding, preAllStanding, False] + standingTest = (self._testIfStanding(*testParams)) + hand["other hand"], allStanding, preAllStanding = standingTest if hand["split"] >= 2: - hand["third hand"], allStanding, preAllStanding = self.testIfStanding(hand["third hand"],allStanding,preAllStanding,False) + testHand = hand["third hand"] + testParams = [testHand, allStanding, preAllStanding, False] + standingTest = (self._testIfStanding(*testParams)) + hand["third hand"], allStanding, preAllStanding = standingTest if hand["split"] >= 3: - hand["fourth hand"], allStanding, preAllStanding = self.testIfStanding(hand["fourth hand"],allStanding,preAllStanding,False) + testHand = hand["fourth hand"] + testParams = [testHand, allStanding, preAllStanding, False] + standingTest = (self._testIfStanding(*testParams)) + hand["fourth hand"], allStanding, preAllStanding = standingTest return hand, allStanding, preAllStanding - # When players try to hit - async def hit(self, ctx, handNumber = 0): + def _blackjackFinish(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* + --------- + finalWinnings: str + The winnings message. + """ + 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"] + + for user in game["user hands"]: + _calcWinningsParams = [ + game["user hands"][user], + dealerValue, + True, + dealerBlackjack, + dealerBusted + ] + winningCalc = (self._calcWinnings(*_calcWinningsParams)) + winnings, netWinnings, reason = winningCalc + + userName = self.bot.databaseFuncs.getName(user) + + if winnings < 0: + if winnings == -1: + finalWinnings += f"{userName} lost 1 GwendoBuck {reason}\n" + else: + moneyLost = -1 * winnings + winningText = f"{userName} lost {moneyLost} GwendoBucks" + winningText += f" {reason}\n" + finalWinnings += winningText + else: + if winnings == 1: + finalWinnings += f"{userName} won 1 GwendoBuck {reason}\n" + else: + winningText = f"{userName} won {winnings} GwendoBucks" + winningText += f" {reason}\n" + finalWinnings += winningText + + self.bot.money.addMoney(user, netWinnings) + + self.bot.database["blackjack games"].delete_one({"_id": channel}) + + return finalWinnings + + def _calcWinnings(self, hand: dict, dealerValue: int, topLevel: bool, + dealerBlackjack: bool, dealerBusted: bool): + """ + Calculate how much a user has won/lost in the blackjack game. + + *Parameters* + ------------ + hand: dict + The hand to calculate the winnings of. + dealerValue: int + The dealer's hand value. + topLevel: bool + If the input hand is _all_ if the player's hands. If + False, it's one of the hands resulting from a split. + dealerBlackjack: bool + If the dealer has a blackjack. + dealerBusted: bool + If the dealer busted. + + *Returns* + --------- + winnings: int + How much the player has won/lost. + netWinnings: 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 + netWinnings = 0 + handValue = self._calcHandValue(hand["hand"]) + + if hand["blackjack"] and not dealerBlackjack: + 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: + _calcWinningsParams = [ + hand["other hand"], + dealerValue, + False, + dealerBlackjack, + dealerBusted + ] + winningsCalc = self._calcWinnings(*_calcWinningsParams) + winningsTemp, netWinningsTemp, reasonTemp = winningsCalc + winnings += winningsTemp + netWinnings += netWinningsTemp + reason += reasonTemp + if hand["split"] >= 2: + _calcWinningsParams = [ + hand["third hand"], + dealerValue, + False, + dealerBlackjack, + dealerBusted + ] + winningsCalc = self._calcWinnings(*_calcWinningsParams) + winningsTemp, netWinningsTemp, reasonTemp = winningsCalc + winnings += winningsTemp + netWinnings += netWinningsTemp + reason += reasonTemp + if hand["split"] >= 3: + _calcWinningsParams = [ + hand["fourth hand"], + dealerValue, + False, + dealerBlackjack, + dealerBusted + ] + winningsCalc = self._calcWinnings(*_calcWinningsParams) + winningsTemp, netWinningsTemp, reasonTemp = winningsCalc + winnings += winningsTemp + netWinnings += netWinningsTemp + reason += reasonTemp + + return winnings, netWinnings, reason + + def _getHandNumber(self, user: dict, handNumber: int): + """ + Get the hand with the given number. + + *Parameters* + ------------ + user: dict + The full hand dict of the user. + handNumber: int + The number of the hand to get. + + *Returns* + --------- + hand: dict + The hand. + handNumber: int + The same as handNumber, except if the user hasn't + split. If the user hasn't split, returns 0. + """ + 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 + + def _isRoundDone(self, game: dict): + """ + Find out if the round is done. + + *Parameters* + ------------ + game: dict + The game to check. + + *Returns* + --------- + roundDone: bool + Whether the round is done. + """ + roundDone = True + + for person in game["user hands"].values(): + if (not person["hit"]) and (not person["standing"]): + roundDone = False + + if person["split"] > 0: + if not person["other hand"]["hit"]: + if not person["other hand"]["standing"]: + roundDone = False + + if person["split"] > 1: + if not person["third hand"]["hit"]: + if not person["third hand"]["standing"]: + roundDone = False + + if person["split"] > 2: + if not person["fourth hand"]["hit"]: + if not person["fourth hand"]["standing"]: + roundDone = False + + return roundDone + + async def _blackjackLoop(self, channel, gameRound: int, gameID: str): + """ + Run blackjack logic and continue if enough time passes. + + *Parameters* + ------------ + channel: guildChannel or DMChannel + The channel the game is happening in. + gameRound: int + The round to start. + gameID: str + The ID of the game. + """ + self.bot.log("Loop "+str(gameRound), str(channel.id)) + + oldImagePath = f"resources/games/oldImages/blackjack{channel.id}" + with open(oldImagePath, "r") as f: + oldImage = await channel.fetch_message(int(f.read())) + + continueData = (self._blackjackContinue(str(channel.id))) + new_message, allStanding, gamedone = continueData + if new_message != "": + self.bot.log(new_message, str(channel.id)) + await channel.send(new_message) + + if not gamedone: + await oldImage.delete() + tablesPath = "resources/games/blackjackTables/" + filePath = f"{tablesPath}blackjackTable{channel.id}.png" + oldImage = await channel.send(file=discord.File(filePath)) + with open(oldImagePath, "w") as f: + f.write(str(oldImage.id)) + + if allStanding: + await asyncio.sleep(5) + else: + await asyncio.sleep(120) + + blackjackGames = self.bot.database["blackjack games"] + game = blackjackGames.find_one({"_id": str(channel.id)}) + + if game is None: + rightRound = False + else: + realRound = game["round"] or -1 + realID = game["gameID"] or -1 + rightRound = gameRound == realRound and gameID == realID + + if rightRound: + if not gamedone: + logMessage = f"Loop {gameRound} calling self._blackjackLoop()" + self.bot.log(logMessage, str(channel.id)) + await self._blackjackLoop(channel, gameRound+1, gameID) + else: + new_message = self._blackjackFinish(str(channel.id)) + await channel.send(new_message) + else: + logMessage = f"Ending loop on round {gameRound}" + self.bot.log(logMessage, str(channel.id)) + + async def hit(self, ctx: discord_slash.context.SlashContext, + handNumber: int = 0): + """ + Hit on a hand. + + *Parameters* + ------------ + ctx: discord_slash.context.SlashContext + The context of the command. + handNumber: int = 0 + The number of the hand to hit. + """ 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}) + blackjackGames = self.bot.database["blackjack games"] + game = blackjackGames.find_one({"_id": channel}) if user in game["user hands"]: userHands = game["user hands"][user] - hand, handNumber = self.getHandNumber(userHands, handNumber) + hand, handNumber = self._getHandNumber(userHands, handNumber) - if hand == None: + if hand is None: logMessage = "They didn't specify a hand" sendMessage = "You need to specify a hand" elif game["round"] <= 0: @@ -197,28 +652,27 @@ class Blackjack(): logMessage = "They're already standing" sendMessage = "You can't hit when you're standing" else: - hand["hand"].append(self.drawCard(channel)) + hand["hand"].append(self._drawCard(channel)) hand["hit"] = True - handValue = self.calcHandValue(hand["hand"]) + 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}}) + handPath = f"user hands.{user}.other hand" elif handNumber == 3: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user+".third hand":hand}}) + handPath = f"user hands.{user}.third hand" elif handNumber == 4: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user+".fourth hand":hand}}) + handPath = f"user hands.{user}.fourth hand" else: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user:hand}}) + handPath = f"user hands.{user}" - roundDone = self.isRoundDone(self.bot.database["blackjack games"].find_one({"_id":channel})) + gameUpdater = {"$set": {handPath: hand}} + blackjackGames.update_one({"_id": channel}, gameUpdater) + game = blackjackGames.find_one({"_id": channel}) + roundDone = self._isRoundDone(game) sendMessage = f"{ctx.author.display_name} hit" logMessage = "They succeeded" @@ -231,22 +685,34 @@ class Blackjack(): if roundDone: gameID = game["gameID"] - self.bot.log("Hit calling self.blackjackLoop()", channel) - await self.blackjackLoop(ctx.channel, game["round"]+1, 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): + async def double(self, ctx: discord_slash.context.SlashContext, + handNumber: int = 0): + """ + Double a hand. + + *Parameters* + ------------ + ctx: discord_slash.context.SlashContext + The context of the command. + handNumber: int = 0 + The number of the hand to double. + """ await self.bot.defer(ctx) channel = str(ctx.channel_id) user = f"#{ctx.author.id}" roundDone = False + blackjackGames = self.bot.database["blackjack games"] - game = self.bot.database["blackjack games"].find_one({"_id":channel}) + game = blackjackGames.find_one({"_id": channel}) if user in game["user hands"]: - hand, handNumber = self.getHandNumber(game["user hands"][user],handNumber) + handParams = [game["user hands"][user], handNumber] + hand, handNumber = self._getHandNumber(*handParams) - if hand == None: + if hand is None: logMessage = "They didn't specify a hand" sendMessage = "You need to specify a hand" elif game["round"] <= 0: @@ -261,42 +727,42 @@ class Blackjack(): elif len(hand["hand"]) != 2: logMessage = "They tried to double after round 1" sendMessage = "You can only double on the first round" + elif self.bot.money.checkBalance(user) < hand["bet"]: + logMessage = "They tried to double without being in the game" + sendMessage = "You can't double when you're not in the game" 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" + 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: + handPath = f"user hands.{user}.other hand" + elif handNumber == 3: + handPath = f"user hands.{user}.third hand" + elif handNumber == 4: + handPath = f"user hands.{user}.fourth hand" else: - self.bot.money.addMoney(user,-1 * bet) + handPath = f"user hands.{user}" - hand["hand"].append(self.drawCard(channel)) - hand["hit"] = True - hand["doubled"] = True - hand["bet"] += bet + gameUpdater = {"$set": {handPath: hand}} + blackjackGames.update_one({"_id": channel}, gameUpdater) - handValue = self.calcHandValue(hand["hand"]) + game = blackjackGames.find_one({"_id": channel}) + roundDone = self._isRoundDone(game) - - 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" + sendMessage = self.bot.longStrings["Blackjack double"] + userName = self.bot.databaseFuncs.getName(user) + sendMessage = sendMessage.format(bet, userName) + 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" @@ -306,23 +772,34 @@ class Blackjack(): if roundDone: gameID = game["gameID"] - self.bot.log("Double calling self.blackjackLoop()", channel) - await self.blackjackLoop(ctx.channel, game["round"]+1, 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): + async def stand(self, ctx: discord_slash.context.SlashContext, + handNumber: int = 0): + """ + Stand on a hand. + + *Parameters* + ------------ + ctx: discord_slash.context.SlashContext + The context of the command. + handNumber: 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}" roundDone = False - game = self.bot.database["blackjack games"].find_one({"_id":channel}) + blackjackGames = self.bot.database["blackjack games"] + game = blackjackGames.find_one({"_id": channel}) - if user in game["user hands"]: + if game is not None and user in game["user hands"]: + handParams = [game["user hands"][user], handNumber] + hand, handNumber = self._getHandNumber(*handParams) - hand, handNumber = self.getHandNumber(game["user hands"][user],handNumber) - - if hand == None: + if hand is None: sendMessage = "You need to specify which hand" logMessage = "They didn't specify a hand" elif game["round"] <= 0: @@ -338,19 +815,18 @@ class Blackjack(): hand["standing"] = True if handNumber == 2: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user+".other hand":hand}}) + handPath = f"user hands.{user}.other hand" elif handNumber == 3: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user+".third hand":hand}}) + handPath = f"user hands.{user}.third hand" elif handNumber == 4: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user+".fourth hand":hand}}) + handPath = f"user hands.{user}.fourth hand" else: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user:hand}}) + handPath = f"user hands.{user}" - roundDone = self.isRoundDone(self.bot.database["blackjack games"].find_one({"_id":channel})) + gameUpdater = {"$set": {handPath: hand}} + blackjackGames.update_one({"_id": channel}, gameUpdater) + game = blackjackGames.find_one({"_id": channel}) + roundDone = self._isRoundDone(game) sendMessage = f"{ctx.author.display_name} is standing" logMessage = "They succeeded" @@ -364,18 +840,29 @@ class Blackjack(): if roundDone: gameID = game["gameID"] - self.bot.log("Stand calling self.blackjackLoop()", channel) - await self.blackjackLoop(ctx.channel, game["round"]+1, 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): + async def split(self, ctx: discord_slash.context.SlashContext, + handNumber: int = 0): + """ + Split a hand. + + *Parameters* + ------------ + ctx: discord_slash.context.SlashContext + The context of the command. + handNumber: int = 0 + The number of the hand to split. + """ 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}) + blackjackGames = self.bot.database["blackjack games"] + game = blackjackGames.find_one({"_id": channel}) if game["user hands"][user]["split"] == 0: hand = game["user hands"][user] @@ -410,7 +897,7 @@ class Blackjack(): sendMessage = "You can only split 3 times" elif hand["hit"]: logMessage = "They've already hit" - sendMessage = "You've already hit" + sendMessage = "You've already hit or split this hand." elif hand["standing"]: logMessage = "They're already standing" sendMessage = "You're already standing" @@ -418,34 +905,37 @@ class Blackjack(): 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]]) + 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" + sendMessage = self.bot.longStrings["Blackjack different cards"] 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) + 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} + "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)) + newHand["hand"].append(self._drawCard(channel)) + hand["hand"].append(self._drawCard(channel)) + hand["hit"] = True - handValue = self.calcHandValue(hand["hand"]) - otherHandValue = self.calcHandValue(newHand["hand"]) + handValue = self._calcHandValue(hand["hand"]) + otherHandValue = self._calcHandValue(newHand["hand"]) if handValue > 21: hand["busted"] = True elif handValue == 21: @@ -457,31 +947,34 @@ class Blackjack(): newHand["blackjack"] = True if handNumber == 2: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user+".other hand":hand}}) + handPath = f"user hands.{user}.other hand" elif handNumber == 3: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user+".third hand":hand}}) + handPath = f"user hands.{user}.third hand" else: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user:hand}}) + handPath = f"user hands.{user}" + + gameUpdater = {"$set": {handPath: hand}} + blackjackGames.update_one({"_id": channel}, gameUpdater) if otherHand == 3: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user+".third hand":newHand}}) + otherHandPath = f"user hands.{user}.third hand" elif otherHand == 4: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user+".fourth hand":newHand}}) + otherHandPath = f"user hands.{user}.fourth hand" else: - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$set":{"user hands."+user+".other hand":newHand}}) + otherHandPath = f"user hands.{user}.other hand" - self.bot.database["blackjack games"].update_one({"_id":channel}, - {"$inc":{"user hands."+user+".split":1}}) + gameUpdater = {"$set": {otherHandPath: newHand}} + blackjackGames.update_one({"_id": channel}, gameUpdater) - roundDone = self.isRoundDone(self.bot.database["blackjack games"].find_one({"_id":channel})) + splitUpdater = {"$inc": {"user hands."+user+".split": 1}} + blackjackGames.update_one({"_id": channel}, splitUpdater) - 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." + game = blackjackGames.find_one({"_id": channel}) + roundDone = self._isRoundDone(game) + + sendMessage = self.bot.longStrings["Blackjack split"] + userName = self.bot.databaseFuncs.getName(user) + sendMessage = sendMessage.format(userName) logMessage = "They succeeded" await ctx.send(sendMessage) @@ -489,21 +982,31 @@ class Blackjack(): if roundDone: gameID = game["gameID"] - self.bot.log("Stand calling self.blackjackLoop()", channel) - await self.blackjackLoop(ctx.channel, game["round"]+1, 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): + async def enterGame(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}) + 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: + if game is None: sendMessage = "There is no game going on in this channel" logMessage = sendMessage elif user in game["user hands"]: @@ -522,78 +1025,97 @@ class Blackjack(): 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)] + self.bot.money.addMoney(user, -1 * bet) + playerHand = [self._drawCard(channel) for _ in range(2)] - handValue = self.calcHandValue(playerHand) + 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":{}} + 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) + 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) + self.bot.log(logMessage) + await ctx.send(sendMessage) - # Starts a game of blackjack - async def start(self, ctx): + 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) blackjackMinCards = 50 - blackjackDecks = 4 + self.bot.log("Starting blackjack game") await ctx.send("Starting a new game of blackjack") cardsLeft = 0 - cards = self.bot.database["blackjack cards"].find_one({"_id":channel}) - if cards != None: + cards = self.bot.database["blackjack cards"].find_one({"_id": channel}) + if cards is not None: cardsLeft = len(cards["cards"]) # Shuffles if not enough cards if cardsLeft < blackjackMinCards: - self.blackjackShuffle(blackjackDecks, channel) + self._blackjackShuffle(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}) + 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: + if game is None: - dealerHand = [self.drawCard(channel),self.drawCard(channel)] + 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} + 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: + 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") + tableImagesPath = "resources/games/blackjackTables/" + emptyTableImagePath = f"resources/games/blackjackTable.png" + newTableImagePath = f"{tableImagesPath}blackjackTable{channel}.png" + copyfile(emptyTableImagePath, newTableImagePath) gameStarted = True if gameStarted: - sendMessage = "Blackjack game started. Use \"/blackjack bet [amount]\" to enter the game within the next 30 seconds." + sendMessage = self.bot.longStrings["Blackjack started"] await ctx.channel.send(sendMessage) - filePath = f"resources/games/blackjackTables/blackjackTable{channel}.png" - oldImage = await ctx.channel.send(file = discord.File(filePath)) + tableImagesPath = "resources/games/blackjackTables/" + filePath = f"{tableImagesPath}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)) @@ -602,365 +1124,346 @@ class Blackjack(): gamedone = False - game = self.bot.database["blackjack games"].find_one({"_id":str(channel)}) + blackjackGames = self.bot.database["blackjack games"] + game = blackjackGames.find_one({"_id": channel}) if len(game["user hands"]) == 0: gamedone = True - await ctx.channel.send("No one entered the game. Ending the game.") + sendMessage = "No one entered the game. Ending the game." + await ctx.channel.send(sendMessage) + gameID = game["gameID"] # Loop of game rounds - if gamedone == False: - self.bot.log("start() calling blackjackLoop()", channel) - await self.blackjackLoop(ctx.channel,1,gameID) + if not gamedone: + self.bot.log("start() calling _blackjackLoop()", channel) + await self._blackjackLoop(ctx.channel, 1, gameID) else: - new_message = self.blackjackFinish(channel) + 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.") + sendMessage = self.bot.longStrings["Blackjack going on"] + await ctx.channel.send(sendMessage) self.bot.log("There was already a game going on") - # Ends the game and calculates winnings - def blackjackFinish(self,channel): - finalWinnings = "*Final Winnings:*\n" + async def hilo(self, ctx: discord_slash.context.SlashContext): + """ + Get the hilo of the blackjack game. - 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): + *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 != None: + 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) - # Shuffles the blackjack deck - async def shuffle(self, ctx): - blackjackDecks = 4 + 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.blackjackShuffle(blackjackDecks,str(channel)) - self.bot.log("Shuffling the blackjack deck...",str(channel)) + self._blackjackShuffle(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. - # Tells you the amount of cards left - async def cards(self, ctx): + *Parameters* + ------------ + ctx: discord_slash.context.SlashContext + The context of the command. + """ channel = ctx.channel_id cardsLeft = 0 - cards = self.bot.database["blackjack cards"].find_one({"_id":str(channel)}) - if cards != None: + blackjackGames = self.bot.database["blackjack cards"] + cards = blackjackGames.find_one({"_id": str(channel)}) + if cards is not None: cardsLeft = len(cards["cards"]) - decksLeft = round(cardsLeft/52,1) - await ctx.send(f"Cards left:\n{cardsLeft} cards, {decksLeft} decks", hidden=True) + decksLeft = round(cardsLeft/52, 1) + sendMessage = f"Cards left:\n{cardsLeft} cards, {decksLeft} decks" + await ctx.send(sendMessage, hidden=True) + class DrawBlackjack(): - def __init__(self,bot): + """ + Draws the blackjack image. + + *Methods* + --------- + drawImage(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. + """ + + def __init__(self, bot): + """Initialize the class.""" self.bot = bot self.BORDER = 100 - self.PLACEMENT = [0,0] - self.ROTATION = 0 + self.PLACEMENT = [2, 1, 3, 0, 4] - def drawImage(self,channel): - self.bot.log("Drawing blackjack table",channel) - game = self.bot.database["blackjack games"].find_one({"_id":channel}) + def drawImage(self, channel: str): + """ + Draw the table image. - 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) + *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}) + + font = ImageFont.truetype('resources/fonts/futura-bold.ttf', 50) + smallFont = ImageFont.truetype('resources/fonts/futura-bold.ttf', 40) + self.SMALLBORDER = 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)") + if not game["all standing"]: + handParams = [ + game["dealer hand"], + True, + False, + False + ] + else: + handParams = [ + game["dealer hand"], + False, + dealerBusted, + dealerBlackjack + ] - table.paste(dealerHand,(800-self.BORDERSmol,20-self.BORDERSmol),dealerHand) + dealerHand = self._drawHand(*handParams) + + pastePosition = (800-self.SMALLBORDER, 20-self.SMALLBORDER) + table.paste(dealerHand, pastePosition, 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)") + handParams = [ + value["hand"], + False, + value["busted"], + value["blackjack"] + ] + userHand = self._drawHand(*handParams) + positionX = 32-self.SMALLBORDER+(384*self.PLACEMENT[x]) - 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) + if value["split"] >= 1: + handParamsTwo = [ + value["other hand"]["hand"], + False, + value["other hand"]["busted"], + value["other hand"]["blackjack"] + ] + userOtherHand = self._drawHand(*handParamsTwo) + + if value["split"] >= 2: + handParamsThree = [ + value["third hand"]["hand"], + False, + value["third hand"]["busted"], + value["third hand"]["blackjack"] + ] + userThirdHand = self._drawHand(*handParamsThree) + + if value["split"] >= 3: + handParamsFour = [ + value["fourth hand"]["hand"], + False, + value["fourth hand"]["busted"], + value["fourth hand"]["blackjack"] + ] + userFourthHand = self._drawHand(*handParamsFour) + + if value["split"] == 3: + positionOne = (positionX, 280-self.SMALLBORDER) + positionTwo = (positionX, 420-self.SMALLBORDER) + positionThree = (positionX, 560-self.SMALLBORDER) + positionFour = (positionX, 700-self.SMALLBORDER) + + table.paste(userHand, positionOne, userHand) + table.paste(userOtherHand, positionTwo, userOtherHand) + table.paste(userThirdHand, positionThree, userThirdHand) + table.paste(userFourthHand, positionFour, userFourthHand) + elif value["split"] == 2: + positionOne = (positionX, 420-self.SMALLBORDER) + positionTwo = (positionX, 560-self.SMALLBORDER) + positionThree = (positionX, 700-self.SMALLBORDER) + + table.paste(userHand, positionOne, userHand) + table.paste(userOtherHand, positionTwo, userOtherHand) + table.paste(userThirdHand, positionThree, userThirdHand) + elif value["split"] == 1: + positionOne = (positionX, 560-self.SMALLBORDER) + positionTwo = (positionX, 700-self.SMALLBORDER) + + table.paste(userHand, positionOne, userHand) + table.paste(userOtherHand, positionTwo, userOtherHand) 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) + positionOne = (positionX, 680-self.SMALLBORDER) + table.paste(userHand, positionOne, userHand) + + textWidth = font.getsize(key)[0] + textX = 32+(384*self.PLACEMENT[x])+117-int(textWidth/2) + black = (0, 0, 0) + white = (255, 255, 255) + + if textWidth < 360: + # Black shadow behind and slightly below white text + textImage.text((textX-3, 1010-3), key, fill=black, font=font) + textImage.text((textX+3, 1010-3), key, fill=black, font=font) + textImage.text((textX-3, 1010+3), key, fill=black, font=font) + textImage.text((textX+3, 1010+3), key, fill=black, font=font) + textImage.text((textX, 1005), key, fill=white, font=font) + else: + textWidth = smallFont.getsize(key)[0] + shadows = [ + (textX-2, 1020-2), + (textX+2, 1020-2), + (textX-2, 1020+2), + (textX+2, 1020+2) + ] + textImage.text(shadows[0], key, fill=black, font=smallFont) + textImage.text(shadows[1], key, fill=black, font=smallFont) + textImage.text(shadows[2], key, fill=black, font=smallFont) + textImage.text(shadows[3], key, fill=black, font=smallFont) + textImage.text((textX, 1015), key, fill=white, font=smallFont) self.bot.log("Saving table image") - table.save("resources/games/blackjackTables/blackjackTable"+channel+".png") + tableImagesPath = "resources/games/blackjackTables/" + tableImagePath = f"{tableImagesPath}blackjackTable{channel}.png" + table.save(tableImagePath) 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) + def _drawHand(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}") + font = ImageFont.truetype('resources/fonts/futura-bold.ttf', 200) + font2 = 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)) + backgroundWidth = (self.BORDER*2)+691+(125*(length-1)) + backgroundSize = (backgroundWidth, (self.BORDER*2)+1065) + background = Image.new("RGBA", backgroundSize, (0, 0, 0, 0)) textImage = ImageDraw.Draw(background) + cardY = self.BORDER+self.PLACEMENT[1] 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) + cardPosition = (self.BORDER+self.PLACEMENT[0], cardY) + background.paste(img, cardPosition, 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) + cardPosition = (125+self.BORDER+self.PLACEMENT[0], cardY) + background.paste(img, cardPosition, 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) + cardPath = f"resources/games/cards/{hand[x].upper()}.png" + img = Image.open(cardPath) + cardPosition = (self.BORDER+(x*125)+self.PLACEMENT[0], cardY) + background.paste(img, cardPosition, img) w, h = background.size textHeight = 290+self.BORDER + white = (255, 255, 255) + black = (0, 0, 0) + red = (255, 50, 50) + gold = (155, 123, 0) - #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) + textWidth = font.getsize("BUSTED")[0] + textX = int(w/2)-int(textWidth/2) + positions = [ + (textX-10, textHeight+20-10), + (textX+10, textHeight+20-10), + (textX-10, textHeight+20+10), + (textX+10, textHeight+20+10), + (textX-5, textHeight-5), + (textX+5, textHeight-5), + (textX-5, textHeight+5), + (textX+5, textHeight+5), + (textX, textHeight) + ] + textImage.text(positions[0], "BUSTED", fill=black, font=font) + textImage.text(positions[1], "BUSTED", fill=black, font=font) + textImage.text(positions[2], "BUSTED", fill=black, font=font) + textImage.text(positions[3], "BUSTED", fill=black, font=font) + textImage.text(positions[4], "BUSTED", fill=white, font=font) + textImage.text(positions[5], "BUSTED", fill=white, font=font) + textImage.text(positions[6], "BUSTED", fill=white, font=font) + textImage.text(positions[7], "BUSTED", fill=white, font=font) + textImage.text(positions[8], "BUSTED", fill=red, font=font) 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) + textWidth = font2.getsize("BLACKJACK")[0] + textX = int(w/2)-int(textWidth/2) + positions = [ + (textX-6, textHeight+20-6), + (textX+6, textHeight+20-6), + (textX-6, textHeight+20+6), + (textX+6, textHeight+20+6), + (textX-3, textHeight-3), + (textX+3, textHeight-3), + (textX-3, textHeight+3), + (textX+3, textHeight+3), + (textX, textHeight) + ] + textImage.text(positions[0], "BLACKJACK", fill=black, font=font2) + textImage.text(positions[1], "BLACKJACK", fill=black, font=font2) + textImage.text(positions[2], "BLACKJACK", fill=black, font=font2) + textImage.text(positions[3], "BLACKJACK", fill=black, font=font2) + textImage.text(positions[4], "BLACKJACK", fill=white, font=font2) + textImage.text(positions[5], "BLACKJACK", fill=white, font=font2) + textImage.text(positions[6], "BLACKJACK", fill=white, font=font2) + textImage.text(positions[7], "BLACKJACK", fill=white, font=font2) + textImage.text(positions[8], "BLACKJACK", fill=gold, font=font2) + resizedSize = (int(w/3.5), int(h/3.5)) + return background.resize(resizedSize, resample=Image.BILINEAR) diff --git a/requirements.txt b/requirements.txt index e66d93c..ce8b57f 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 @@ -21,7 +21,7 @@ idna==2.10 IMDbPY==2020.9.25 isort==5.8.0 jaraco.context==4.0.0 -lark-parser==0.9.0 +lark-parser==0.11.2 lazy-object-proxy==1.6.0 lxml==4.6.3 mccabe==0.6.1 diff --git a/resources/longStrings.json b/resources/longStrings.json index a7b418f..1458e3d 100644 --- a/resources/longStrings.json +++ b/resources/longStrings.json @@ -1,4 +1,12 @@ { "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" + "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." } \ No newline at end of file diff --git a/utils/utilFunctions.py b/utils/utilFunctions.py index 6fea627..a472a21 100644 --- a/utils/utilFunctions.py +++ b/utils/utilFunctions.py @@ -13,12 +13,12 @@ Contains utility functions used by parts of the bot. newString: str) -> str emojiToCommand(emoji: str) -> str """ -import json -import logging -import os -import sys -import imdb -from .helperClasses import Options +import json # Used by longString(), getParams() and makeFiles() +import logging # Used for logging +import os # Used by makeFiles() to check if files exist +import sys # Used to specify printing for logging +import imdb # Used to disable logging for the module +from .helperClasses import Options # Used by getParams() # All of this is logging configuration From 872ad1aa6d6b6f730ebd7ff19d0bd58408603ac3 Mon Sep 17 00:00:00 2001 From: NikolajDanger Date: Sat, 17 Apr 2021 21:30:46 +0200 Subject: [PATCH 03/10] :dollar: Money --- funcs/games/money.py | 151 +++++++++++++++++++++++++++++++++---------- 1 file changed, 116 insertions(+), 35 deletions(-) diff --git a/funcs/games/money.py b/funcs/games/money.py index dea2812..c626a91 100644 --- a/funcs/games/money.py +++ b/funcs/games/money.py @@ -1,70 +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 - # Returns the account balance for a user - def checkBalance(self, user): + 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") - userData = self.database["users"].find_one({"_id":user}) + userData = self.database["users"].find_one({"_id": user}) - if userData != None: + if userData is not None: return userData["money"] - else: return 0 + else: + return 0 - async def sendBalance(self, ctx): + 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)) + userName = ctx.author.display_name if response == 1: - new_message = ctx.author.display_name + " has " + str(response) + " GwendoBuck" + new_message = f"{userName} has {response} GwendoBuck" else: - new_message = ctx.author.display_name + " has " + str(response) + " GwendoBucks" + new_message = f"{userName} has {response} GwendoBucks" await ctx.send(new_message) # Adds money to the account of a user - def addMoney(self,user,amount): + 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") - userData = self.database["users"].find_one({"_id":user}) + userData = self.database["users"].find_one({"_id": user}) - if userData != None: - self.database["users"].update_one({"_id":user},{"$inc":{"money":amount}}) + if userData is not None: + updater = {"$inc": {"money": amount}} + self.database["users"].update_one({"_id": user}, updater) else: - self.database["users"].insert_one({"_id":user,"user name":self.bot.databaseFuncs.getName(user),"money":amount}) + newUser = { + "_id": user, + "user name": self.bot.databaseFuncs.getName(user), + "money": amount + } + self.database["users"].insert_one(newUser) # Transfers money from one user to another - async def giveMoney(self, ctx, user, amount): + 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.databaseFuncs.getID(username) == None: + if self.bot.databaseFuncs.getID(username) is 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} + userID = f"#{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}"}) + userid = f"#{ctx.author.id}" + userData = self.database["users"].find_one({"_id": userid}) 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: + 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 userData is None or userData["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}") From 0e714b3f5cda3ccd0f451ad4aadc3cb6ae26ce0c Mon Sep 17 00:00:00 2001 From: NikolajDanger Date: Sat, 17 Apr 2021 21:44:00 +0200 Subject: [PATCH 04/10] :dice: Games container --- funcs/games/gamesContainer.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/funcs/games/gamesContainer.py b/funcs/games/gamesContainer.py index 203209a..0169cc8 100644 --- a/funcs/games/gamesContainer.py +++ b/funcs/games/gamesContainer.py @@ -1,3 +1,13 @@ +""" +Has a container for game functions. + +*Classes* +--------- + Games + Container for game functions. +""" + + from .invest import Invest from .trivia import Trivia from .blackjack import Blackjack @@ -5,8 +15,29 @@ from .connectFour 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. + connectFour + 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) From 4c6c8c2ea36ce99e2249294a249004832ac348bf Mon Sep 17 00:00:00 2001 From: NikolajDanger Date: Sun, 18 Apr 2021 14:06:39 +0200 Subject: [PATCH 05/10] :moneybag: Invest --- funcs/games/invest.py | 297 +++++++++++++++++++++++++++---------- resources/longStrings.json | 4 +- 2 files changed, 220 insertions(+), 81 deletions(-) diff --git a/funcs/games/invest.py b/funcs/games/invest.py index 75bb33a..2501353 100644 --- a/funcs/games/invest.py +++ b/funcs/games/invest.py @@ -1,111 +1,243 @@ -import discord +""" +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): + 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.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}) + def getPortfolio(self, user: str): + """ + Get the stock portfolio of a user. - if userInvestments in [None,{}]: - return f"{self.bot.databaseFuncs.getName(user)} does not have a stock portfolio." + *Parameters* + ------------ + user: str + The id of the user to get the portfolio of. + + *Returns* + --------- + portfolio: str + The portfolio. + """ + investmentsDatabase = self.bot.database["investments"] + userInvestments = investmentsDatabase.find_one({"_id": user}) + + userName = self.bot.databaseFuncs.getName(user) + + if userInvestments in [None, {}]: + return f"{userName} does not have a stock portfolio." else: - portfolio = f"**Stock portfolio for {self.bot.databaseFuncs.getName(user)}**" + portfolio = f"**Stock portfolio for {userName}**" 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)})" + 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): - 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}) + def buyStock(self, user: str, stock: str, buyAmount: int): + """ + Buy an amount of a specific stock. - self.bot.money.addMoney(user,-1*buyAmount) - stock = stock.upper() + *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. - 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: + *Returns* + --------- + sendMessage: 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: + investmentsDatabase = self.bot.database["investments"] + stockPrice = self.getPrice(stock) + userInvestments = investmentsDatabase.find_one({"_id": user}) - def sellStock(self, user : str, stock : str, sellAmount : int): - if sellAmount > 0: + self.bot.money.addMoney(user, -1*buyAmount) + stock = stock.upper() - userInvestments = self.bot.database["investments"].find_one({"_id":user})["investments"] + 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 + + valuePath = f"investments.{stock}.value at purchase" + updater = {"$set": {valuePath: stockPrice}} + investmentsDatabase.update_one({"_id": user}, updater) + + purchasedPath = f"investments.{stock}.purchased" + updater = {"$set": {purchasedPath: newAmount}} + investmentsDatabase.update_one({"_id": user}, updater) + + if value["purchased for"] != "?": + purchasedForPath = f"investments.{stock}.purchased for" + updater = {"$set": {purchasedForPath: buyAmount}} + investmentsDatabase.update_one({"_id": user}, updater) + else: + updater = { + "$set": { + "investments.{stock}": { + "purchased": buyAmount, + "value at purchase": stockPrice, + "purchased for": buyAmount + } + } + } + investmentsDatabase.update_one({"_id": user}, updater) + else: + newUser = { + "_id": user, + "investments": { + stock: { + "purchased": buyAmount, + "value at purchase": stockPrice, + "purchased for": buyAmount + } + } + } + investmentsDatabase.insert_one(newUser) + + userName = self.bot.databaseFuncs.getName(user) + sendMessage = "{} bought {} GwendoBucks worth of {} stock" + sendMessage = sendMessage.format(userName, buyAmount, stock) + return sendMessage + + 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* + --------- + sendMessage: str + The message to return to the user. + """ + if sellAmount <= 0: + return "no" + else: + investmentsDatabase = self.bot.database["investments"] + userData = investmentsDatabase.find_one({"_id": user}) + userInvestments = userData["investments"] stock = stock.upper() - if userInvestments != None and stock in userInvestments: + if userInvestments is not 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}}) + priceChange = (stockPrice / value["value at purchase"]) + purchasedAmount = int(priceChange * value["purchased"]) + purchasedPath = f"investments.{stock}.purchased" + updater = {"$set": {purchasedPath: purchasedAmount}} + investmentsDatabase.update_one({"_id": user}, updater) + valueAtPurchasePath = f"investments.{stock}.value at purchase" + updater = {"$set": {valueAtPurchasePath: stockPrice}} + investmentsDatabase.update_one({"_id": user}, updater) if value["purchased"] >= sellAmount: - self.bot.money.addMoney(user,sellAmount) + self.bot.money.addMoney(user, sellAmount) if sellAmount < value["purchased"]: - self.bot.database["investments"].update_one({"_id":user}, - {"$inc":{"investments."+stock+".purchased" : -sellAmount}}) + purchasedPath = f"investments.{stock}.purchased" + updater = {"$inc": {purchasedPath: -sellAmount}} + investmentsDatabase.update_one({"_id": user}, updater) - self.bot.database["investments"].update_one({"_id":user}, - {"$set":{"investments."+stock+".purchased for" : "?"}}) + purchasedForPath = f"investments.{stock}.purchased for" + updater = {"$set": {purchasedForPath: "?"}} + investmentsDatabase.update_one({"_id": user}, updater) else: - self.bot.database["investments"].update_one({"_id":user}, - {"$unset":{"investments."+stock:""}}) + updater = {"$unset": {f"investments.{stock}": ""}} + investmentsDatabase.update_one({"_id": user}, updater) - return f"{self.bot.databaseFuncs.getName(user)} sold {sellAmount} GwendoBucks worth of {stock} stock" + userName = self.bot.databaseFuncs.getName(user) + sendMessage = "{} sold {} GwendoBucks worth of {} stock" + return sendMessage.format(userName, 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" - else: - return "no" - async def parseInvest(self, ctx, parameters): + 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}" @@ -116,34 +248,39 @@ class Invest(): else: price = self.getPrice(commands[1]) if price == 0: - response = f"{commands[1].upper()} is not traded on the american market." + response = "{} is not traded on the american market." + response = response.format(commands[0].upper()) else: - price = f"{price:,}".replace(",",".") - response = f"The current {commands[1].upper()} stock is valued at **{price}** GwendoBucks" + price = f"{price:,}".replace(",", ".") + response = self.bot.longStrings["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])) + 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." + response = self.bot.longStrings["Stock parameters"] 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]\"" + response = self.sellStock(user, commands[1], int(commands[2])) else: - response = "You must give both a stock name and an amount of GwendoBucks you wish to sell stocks for." + response = self.bot.longStrings["Stock parameters"] else: response = "Incorrect parameters" if response.startswith("**"): responses = response.split("\n") - em = discord.Embed(title=responses[0],description="\n".join(responses[1:]),colour=0x00FF00) + 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/resources/longStrings.json b/resources/longStrings.json index 1458e3d..d1e1a0e 100644 --- a/resources/longStrings.json +++ b/resources/longStrings.json @@ -8,5 +8,7 @@ "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." + "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." } \ No newline at end of file From 886b73391485c84701539080246bfcaaa467491f Mon Sep 17 00:00:00 2001 From: NikolajDanger Date: Sun, 18 Apr 2021 14:42:21 +0200 Subject: [PATCH 06/10] :sparkles: Trivia --- funcs/games/trivia.py | 207 ++++++++++++++++++++++++++----------- resources/longStrings.json | 4 +- 2 files changed, 150 insertions(+), 61 deletions(-) diff --git a/funcs/games/trivia.py b/funcs/games/trivia.py index eeb4003..e01fc1d 100644 --- a/funcs/games/trivia.py +++ b/funcs/games/trivia.py @@ -1,94 +1,175 @@ -import json -import urllib -import random -import asyncio +""" +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 - # 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}) + def triviaStart(self, channel: str): + """ + Start a game of trivia. - self.bot.log("Trying to find a trivia question for "+channel) + 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. - if question == None: - with urllib.request.urlopen("https://opentdb.com/api.php?amount=10&type=multiple") as response: + *Parameters* + ------------ + channel: str + The id of the channel to start the game in + + *Returns* + --------- + sendMessage: 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()) - self.bot.log("Found the question \""+data["results"][0]["question"]+"\"") + 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 = answers.index(data["results"][0]["correct_answer"]) + 97 + correctAnswer = data["results"][0]["correct_answer"] + correctAnswer = answers.index(correctAnswer) + 97 - self.bot.database["trivia questions"].insert_one({"_id":channel,"answer" : str(chr(correctAnswer)),"players" : {}}) + newQuestion = { + "_id": channel, + "answer": str(chr(correctAnswer)), + "players": {} + } + triviaQuestions.insert_one(newQuestion) - replacements = {"'": "\'", - """: "\"", - "“": "\"", - "”": "\"", - "é": "é"} + replacements = { + "'": "\'", + """: "\"", + "“": "\"", + "”": "\"", + "é": "é" + } question = data["results"][0]["question"] for key, value in replacements.items(): - question = question.replace(key,value) + question = question.replace(key, value) for answer in answers: for key, value in replacements.items(): - answer = answer.replace(key,value) + 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)", "", "" + logMessage = "There was already a trivia question for that channel" + self.bot.log(logMessage) + return self.bot.longStrings["Trivia going on"], "", "" - # 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}) + def triviaAnswer(self, user: str, channel: str, command: str): + """ + Answer the current trivia question. - 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) + *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. - self.bot.database["trivia questions"].update_one({"_id":channel},{"$set":{"players."+user : command}}) + *Returns* + --------- + sendMessage: str + The message to send if the function failed. + """ + triviaQuestions = self.bot.database["trivia questions"] + question = triviaQuestions.find_one({"_id": channel}) - 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)" + 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("I didn't quite understand that (error code 1103)") - return "I didn't quite understand that (error code 1103)" + self.bot.log(f"{user} answered the question in {channel}") + updater = {"$set": {f"players.{user}": command}} + triviaQuestions.update_one({"_id": channel}, updater) + return None - # 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}) + 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 != None: + if question is not None: for player, answer in question["players"].items(): if answer == question["answer"]: - self.bot.money.addMoney(player,1) - - + self.bot.money.addMoney(player, 1) else: - self.bot.log("Couldn't find the question (error code 1102)") + self.bot.log("Couldn't find the questio") return None - async def triviaParse(self, ctx, answer): + 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(str(ctx.channel_id)) + question, options, correctAnswer = self.triviaStart(channelId) if options != "": results = "**"+question+"**\n" for x, option in enumerate(options): @@ -98,21 +179,27 @@ class Trivia(): await asyncio.sleep(60) - self.triviaCountPoints(str(ctx.channel_id)) + self.triviaCountPoints(channelId) - self.bot.databaseFuncs.deleteGame("trivia questions",str(ctx.channel_id)) + deleteGameParams = ["trivia questions", channelId] + self.bot.databaseFuncs.deleteGame(*deleteGameParams) - 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") + self.bot.log("Time's up for the trivia question", channelId) + sendMessage = self.bot.longStrings["Trivia time up"] + formatParams = [chr(correctAnswer), options[correctAnswer-97]] + sendMessage = sendMessage.format(*formatParams) + await ctx.send(sendMessage) 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}**") + elif answer in ["a", "b", "c", "d"]: + userId = f"#{ctx.author.id}" + response = self.triviaAnswer(userId, channelId, answer) + if response is None: + userName = ctx.author.display_name + await ctx.send(f"{userName} 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)") + self.bot.log("I didn't understand that", channelId) + await ctx.send("I didn't understand that") diff --git a/resources/longStrings.json b/resources/longStrings.json index d1e1a0e..188fc7f 100644 --- a/resources/longStrings.json +++ b/resources/longStrings.json @@ -10,5 +10,7 @@ "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." + "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" } \ No newline at end of file From 22925833114d42888d53513f56a2c5f59194d636 Mon Sep 17 00:00:00 2001 From: NikolajDanger Date: Sun, 18 Apr 2021 20:09:13 +0200 Subject: [PATCH 07/10] :red_circle: Connect four --- funcs/games/connectFour.py | 1111 ++++++++++++++++++++++++++---------- resources/longStrings.json | 4 +- 2 files changed, 801 insertions(+), 314 deletions(-) diff --git a/funcs/games/connectFour.py b/funcs/games/connectFour.py index 88a8024..c0d511d 100644 --- a/funcs/games/connectFour.py +++ b/funcs/games/connectFour.py @@ -1,14 +1,42 @@ -import random -import copy -import math -import discord +""" +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 +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 +from typing import Union # Used for typehints + +ROWCOUNT = 6 +COLUMNCOUNT = 7 -from PIL import Image, ImageDraw, ImageFont class ConnectFour(): - def __init__(self,bot): + """ + Deals with connect four commands and logic. + + *Methods* + --------- + start(ctx: SlashContext, opponent: Union[int, Discord.User]) + placePiece(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.draw = DrawConnectFour(bot) self.AISCORES = { "middle": 3, "two in a row": 10, @@ -19,67 +47,84 @@ class ConnectFour(): "win": 10000, "avoid losing": 100 } + self.REACTIONS = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣"] - self.ROWCOUNT = 6 - self.COLUMNCOUNT = 7 + async def start(self, ctx: SlashContext, + opponent: Union[int, discord.User]): + """ + Start a game of connect four. - # Starts the game - async def start(self, ctx, opponent): + *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}) + 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" + if game is not None: + sendMessage = self.bot.longStrings["Connect 4 going on"] 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) + elif 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 challenged a difficulty that doesn't exist" + canStart = False + elif type(opponent) == discord.User: + 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 = "Difficulty doesn't exist" - logMessage = "They tried to play against a difficulty that doesn't exist" + 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 - 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)] + x, y = COLUMNCOUNT, ROWCOUNT + board = [[0 for _ in range(x)] for _ in range(y)] 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} + 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) @@ -101,23 +146,42 @@ class ConnectFour(): # 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)) + boardsPath = "resources/games/connect4Boards/" + filePath = f"{boardsPath}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: + oldImagesPath = "resources/games/oldImages/" + oldImagePath = f"{oldImagesPath}connectFour{ctx.channel_id}" + with open(oldImagePath, "w") as f: f.write(str(oldImage.id)) if gwendoTurn: - await self.connectFourAI(ctx) + await self._connectFourAI(ctx) else: - reactions = ["1️⃣","2️⃣","3️⃣","4️⃣","5️⃣","6️⃣","7️⃣"] - for reaction in reactions: + for reaction in self.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): + async def placePiece(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) - game = self.bot.database["connect 4 games"].find_one({"_id":channel}) + connect4Games = self.bot.database["connect 4 games"] + game = connect4Games.find_one({"_id": channel}) playerNumber = game["players"].index(user)+1 userName = self.bot.databaseFuncs.getName(user) placedPiece = False @@ -127,39 +191,48 @@ class ConnectFour(): logMessage = "There was no game in the channel" else: board = game["board"] - board = self.placeOnBoard(board, playerNumber, column) + 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}}) + updater = {"$set": {"board": board}} + connect4Games.update_one({"_id": channel}, updater) + turn = (game["turn"]+1) % 2 + updater = {"$set": {"turn": turn}} + connect4Games.update_one({"_id": channel}, updater) self.bot.log("Checking for win") - won, winDirection, winCoordinates = self.isWon(board) + 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}}) + updater = {"$set": {"winner": won}} + connect4Games.update_one({"_id": channel}, updater) + updater = {"$set": {"win direction": winDirection}} + connect4Games.update_one({"_id": channel}, updater) + updater = {"$set": {"win coordinates": winCoordinates}} + connect4Games.update_one({"_id": channel}, updater) - sendMessage = f"{userName} placed a piece in column {column+1} and won." + sendMessage = "{} placed a piece in column {} and won. " + sendMessage = sendMessage.format(userName, column+1) 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" + sendMessage += "Adding {} GwendoBucks to their account" + sendMessage = sendMessage.format(winAmount) 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" + otherUserId = game["players"][turn] + otherUserName = self.bot.databaseFuncs.getName(otherUserId) + sendMessage = self.bot.longStrings["Connect 4 placed"] + formatParams = [userName, column+1, otherUserName] + sendMessage = sendMessage.format(*formatParams) logMessage = "They placed the piece" gwendoTurn = (game["players"][turn] == f"#{self.bot.user.id}") @@ -172,7 +245,8 @@ class ConnectFour(): if placedPiece: self.draw.drawImage(channel) - with open(f"resources/games/oldImages/connectFour{channel}", "r") as f: + oldImagePath = f"resources/games/oldImages/connectFour{channel}" + with open(oldImagePath, "r") as f: oldImage = await ctx.channel.fetch_message(int(f.read())) if oldImage is not None: @@ -181,56 +255,35 @@ class ConnectFour(): 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)) + oldImage = await ctx.channel.send(file=discord.File(filePath)) if gameWon: - self.endGame(channel) + self._endGame(channel) else: - with open(f"resources/games/oldImages/connectFour{channel}", "w") as f: + with open(oldImagePath, "w") as f: f.write(str(oldImage.id)) if gwendoTurn: - await self.connectFourAI(ctx) + await self._connectFourAI(ctx) else: - reactions = ["1️⃣","2️⃣","3️⃣","4️⃣","5️⃣","6️⃣","7️⃣"] - for reaction in reactions: + for reaction in self.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 + async def surrender(self, ctx: SlashContext): + """ + Surrender a connect four game. - 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): + *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}) + 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 + winnerIndex = (loserIndex+1) % 2 winnerID = game["players"][winnerIndex] winnerName = self.bot.databaseFuncs.getName(winnerID) @@ -242,7 +295,8 @@ class ConnectFour(): sendMessage += f" Adding {reward} to their account" await ctx.send(sendMessage) - with open(f"resources/games/oldImages/connectFour{channel}", "r") as f: + oldImagePath = f"resources/games/oldImages/connectFour{channel}" + with open(oldImagePath, "r") as f: oldImage = await ctx.channel.fetch_message(int(f.read())) if oldImage is not None: @@ -250,142 +304,219 @@ class ConnectFour(): else: self.bot.log("The old image was already deleted") - self.endGame(channel) + 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): + def _placeOnBoard(self, board: dict, player: int, column: int): + """ + Place a piece in a given board. + + This differs from placePiece() 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. + """ + placementX, placementY = -1, column + + for x, line in enumerate(board): + if line[column] == 0: + placementX = x + break + + board[placementX][placementY] = player + + if placementX == -1: + return None + else: + return board + + def _endGame(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.bot.databaseFuncs.deleteGame("connect 4 games", channel) + + def _isWon(self, board: dict): won = 0 winDirection = "" - winCoordinates = [0,0] + winCoordinates = [0, 0] - for row in range(self.ROWCOUNT): - for place in range(self.COLUMNCOUNT): + for row in range(ROWCOUNT): + for place in range(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]] + if place <= COLUMNCOUNT-4: + pieceOne = board[row][place+1] + pieceTwo = board[row][place+2] + pieceThree = board[row][place+3] + + pieces = [pieceOne, pieceTwo, pieceThree] else: pieces = [0] if all(x == piecePlayer for x in pieces): won = piecePlayer winDirection = "h" - winCoordinates = [row,place] + winCoordinates = [row, place] # Checks vertical - if row <= self.ROWCOUNT-4: - pieces = [board[row+1][place],board[row+2][place],board[row+3][place]] + if row <= ROWCOUNT-4: + pieceOne = board[row+1][place] + pieceTwo = board[row+2][place] + pieceThree = board[row+3][place] + + pieces = [pieceOne, pieceTwo, pieceThree] else: pieces = [0] if all(x == piecePlayer for x in pieces): won = piecePlayer winDirection = "v" - winCoordinates = [row,place] + 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]] + goodRow = (row <= ROWCOUNT-4) + goodColumn = (place <= COLUMNCOUNT-4) + if goodRow and goodColumn: + pieceOne = board[row+1][place+1] + pieceTwo = board[row+2][place+2] + pieceThree = board[row+3][place+3] + + pieces = [pieceOne, pieceTwo, pieceThree] else: pieces = [0] if all(x == piecePlayer for x in pieces): won = piecePlayer winDirection = "r" - winCoordinates = [row,place] + 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]] + if row <= ROWCOUNT-4 and place >= 3: + pieceOne = board[row+1][place-1] + pieceTwo = board[row+2][place-2] + pieceThree = board[row+3][place-3] + + pieces = [pieceOne, pieceTwo, pieceThree] else: pieces = [0] if all(x == piecePlayer for x in pieces): won = piecePlayer winDirection = "l" - winCoordinates = [row,place] - + winCoordinates = [row, place] return won, winDirection, winCoordinates - # Plays as the AI - async def connectFourAI(self, ctx): + async def _connectFourAI(self, ctx: SlashContext): + def outOfRange(possibleScores: list): + allowedRange = max(possibleScores)*(1-0.1) + moreThanOne = len(possibleScores) != 1 + return (min(possibleScores) <= allowedRange) and moreThanOne + channel = str(ctx.channel.id) self.bot.log("Figuring out best move") - game = self.bot.database["connect 4 games"].find_one({"_id":channel}) + 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): + scores = [int(-math.inf) for _ in range(COLUMNCOUNT)] + for column in range(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) + testBoard = self._placeOnBoard(testBoard, player, column) + if testBoard is not None: + _minimaxParams = [ + testBoard, + difficulty, + player % 2+1, + player, + int(-math.inf), + int(math.inf), + False + ] + scores[column] = await self._minimax(*_minimaxParams) 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: + while outOfRange(possibleScores): possibleScores.remove(min(possibleScores)) - highest_score = random.choice(possibleScores) + highestScore = random.choice(possibleScores) - bestColumns = [i for i, x in enumerate(scores) if x == highest_score] + bestColumns = [i for i, x in enumerate(scores) if x == highestScore] placement = random.choice(bestColumns) await self.placePiece(ctx, f"#{self.bot.user.id}", placement) - # Calculates points for a board - def AICalcPoints(self,board,player): + def _AICalcPoints(self, board: dict, player: int): score = 0 - otherPlayer = player%2+1 + otherPlayer = player % 2+1 # Adds points for middle placement # Checks horizontal - for row in range(self.ROWCOUNT): + for row in range(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): + for place in range(COLUMNCOUNT-3): window = rowArray[place:place+4] - score += self.evaluateWindow(window,player,otherPlayer) + score += self._evaluateWindow(window, player, otherPlayer) # Checks Vertical - for column in range(self.COLUMNCOUNT): + for column in range(COLUMNCOUNT): columnArray = [int(i[column]) for i in list(board)] - for place in range(self.ROWCOUNT-3): + for place in range(ROWCOUNT-3): window = columnArray[place:place+4] - score += self.evaluateWindow(window,player,otherPlayer) + 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 row in range(ROWCOUNT-3): + for place in range(COLUMNCOUNT-3): + pieceOne = board[row][place] + pieceTwo = board[row+1][place+1] + pieceThree = board[row+2][place+2] + pieceFour = board[row+3][place+3] - 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) + window = [pieceOne, pieceTwo, pieceThree, pieceFour] + score += self._evaluateWindow(window, player, otherPlayer) + for place in range(3, COLUMNCOUNT): + pieceOne = board[row][place] + pieceTwo = board[row+1][place-1] + pieceThree = board[row+2][place-2] + pieceFour = board[row+3][place-3] - ## Checks if anyone has won - #won = isWon(board)[0] - - ## Add points if AI wins - #if won == player: - # score += self.AISCORES["win"] + window = [pieceOne, pieceTwo, pieceThree, pieceFour] + score += self._evaluateWindow(window, player, otherPlayer) return score - def evaluateWindow(self, window,player,otherPlayer): + def _evaluateWindow(self, window: list, player: int, otherPlayer: int): if window.count(player) == 4: return self.AISCORES["win"] elif window.count(player) == 3 and window.count(0) == 1: @@ -397,76 +528,498 @@ class ConnectFour(): 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)) + async def _minimax(self, board: dict, depth: int, player: int, + originalPlayer: int, alpha: int, beta: int, + maximizingPlayer: 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. + originalPlayer: int + The AI player. + alpha, beta: int + Used with alpha/beta pruning, in order to prune options + that will never be picked. + maximizingPlayer: 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.AICalcPoints(board,originalPlayer) - # The depth is how many moves ahead the computer checks. This value is the difficulty. + 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): + value = int(-math.inf) + for column in range(0, 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 + testBoard = self._placeOnBoard(testBoard, player, column) + if testBoard is not None: + _minimaxParams = [ + testBoard, + depth-1, + (player % 2)+1, + originalPlayer, + alpha, + beta, + False + ] + evaluation = await self._minimax(*_minimaxParams) + 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): + value = int(math.inf) + for column in range(0, 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 + testBoard = self._placeOnBoard(testBoard, player, column) + if testBoard is not None: + _minimaxParams = [ + testBoard, + depth-1, + (player % 2)+1, + originalPlayer, + alpha, + beta, + True + ] + evaluation = await self._minimax(*_minimaxParams) + if evaluation < -9000: + evaluation += self.AISCORES["avoid losing"] + value = min(value, evaluation) + beta = min(beta, evaluation) + if beta <= alpha: + break return value -class drawConnectFour(): + +class DrawConnectFour(): + """ + Functions for drawing the connect four board. + + *Methods* + --------- + drawImage(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 winbarAlpha. + """ + def __init__(self, bot): + """Initialize the class.""" self.bot = bot + 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) + winbarAlpha = 160 + self.WINBARCOLOR = (250, 250, 250, 255) + self.PIECESIZE = 300 + fontPath = "resources/fonts/futura-bold.ttf" + + self.FONT = ImageFont.truetype(fontPath, self.TEXTSIZE) + + boardWidth = self.WIDTH-(2*(self.BORDER+self.GRIDBORDER)) + boardHeight = self.HEIGHT-(2*(self.BORDER+self.GRIDBORDER)) + boardSize = [boardWidth, boardHeight] + + pieceWidth = boardSize[0]//COLUMNCOUNT + pieceHeight = boardSize[1]//ROWCOUNT + self.PIECEGRIDSIZE = [pieceWidth, pieceHeight] + + pieceStart = (self.BORDER+self.GRIDBORDER)-(self.PIECESIZE//2) + self.PIECESTARTX = pieceStart+(self.PIECEGRIDSIZE[0]//2) + self.PIECESTARTY = pieceStart+(self.PIECEGRIDSIZE[1]//2) + + self.WINBARWHITE = (255, 255, 255, winbarAlpha) + # Draws the whole thing - def drawImage(self, channel): + def drawImage(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}) + 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) + imageSize = (self.WIDTH, self.HEIGHT+self.BOTTOMBORDER) + background = Image.new("RGB", imageSize, self.BACKGROUNDCOLOR) + d = ImageDraw.Draw(background, "RGBA") - fnt = ImageFont.truetype('resources/fonts/futura-bold.ttf', exampleCircles) + self._drawBoard(d) - 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) + self._drawPieces(d, board) + if game["winner"] != 0: + self._drawWin(background, game) + + self._drawFooter(d, game) + + background.save(f"resources/games/connect4Boards/board{channel}.png") + + def _drawBoard(self, d: ImageDraw): + # This whole part was the easiest way to make a rectangle with + # rounded corners and an outline + drawParams = { + "fill": self.BOARDCOLOR, + "outline": self.BOARDOUTLINECOLOR, + "width": self.BOARDOUTLINESIZE + } + + # - Corners -: + cornerPositions = [ + [ + ( + 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 + ) + ] + ] + + d.ellipse(cornerPositions[0], **drawParams) + d.ellipse(cornerPositions[1], **drawParams) + d.ellipse(cornerPositions[2], **drawParams) + d.ellipse(cornerPositions[3], **drawParams) + + # - Rectangle -: + rectanglePositions = [ + [ + ( + 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)) + ) + ] + ] + d.rectangle(rectanglePositions[0], **drawParams) + d.rectangle(rectanglePositions[1], **drawParams) + + # - Removing outline on the inside -: + cleanUpPositions = [ + [ + ( + 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)) + ) + ] + ] + d.rectangle(cleanUpPositions[0], fill=self.BOARDCOLOR) + d.rectangle(cleanUpPositions[1], fill=self.BOARDCOLOR) + d.rectangle(cleanUpPositions[2], fill=self.BOARDCOLOR) + + def _drawPieces(self, d: ImageDraw, board: list): + for x, line in enumerate(board): + for y, piece in enumerate(line): + + if piece == 1: + pieceColor = self.PLAYER1COLOR + outlineWidth = self.PIECEOUTLINESIZE + outlineColor = self.PIECEOUTLINECOLOR + elif piece == 2: + pieceColor = self.PLAYER2COLOR + outlineWidth = self.PIECEOUTLINESIZE + outlineColor = self.PIECEOUTLINECOLOR + else: + pieceColor = self.BACKGROUNDCOLOR + outlineWidth = self.EMPTYOUTLINESIZE + outlineColor = self.EMPTYOUTLINECOLOR + + startX = self.PIECESTARTX + self.PIECEGRIDSIZE[0]*y + startY = self.PIECESTARTY + self.PIECEGRIDSIZE[1]*x + + position = [ + (startX, startY), + (startX+self.PIECESIZE, startY+self.PIECESIZE) + ] + + pieceParams = { + "fill": pieceColor, + "outline": outlineColor, + "width": outlineWidth + } + d.ellipse(position, **pieceParams) + + def _drawWin(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 + startX = start + self.PIECEGRIDSIZE[0]*coordinates[1] + startY = start + self.PIECEGRIDSIZE[1]*coordinates[0] + a = (self.PIECEGRIDSIZE[0]*4-self.GRIDBORDER-self.BORDER)**2 + b = (self.PIECEGRIDSIZE[1]*4-self.GRIDBORDER-self.BORDER)**2 + diagonalLength = (math.sqrt(a+b))/self.PIECEGRIDSIZE[0] + sizeRatio = self.PIECEGRIDSIZE[1]/self.PIECEGRIDSIZE[0] + diagonalAngle = math.degrees(math.atan(sizeRatio)) + + if game["win direction"] == "h": + imageSize = (self.PIECEGRIDSIZE[0]*4, self.PIECEGRIDSIZE[1]) + winBar = Image.new("RGBA", imageSize, (0, 0, 0, 0)) + winD = ImageDraw.Draw(winBar) + drawPositions = [ + [ + (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]) + ] + ] + winD.ellipse(drawPositions[0], fill=self.WINBARWHITE) + winD.ellipse(drawPositions[1], fill=self.WINBARWHITE) + winD.rectangle(drawPositions[2], fill=self.WINBARWHITE) + + elif game["win direction"] == "v": + imageSize = (self.PIECEGRIDSIZE[0], self.PIECEGRIDSIZE[1]*4) + winBar = Image.new("RGBA", imageSize, (0, 0, 0, 0)) + winD = ImageDraw.Draw(winBar) + drawPositions = [ + [ + (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)) + ] + ] + winD.ellipse(drawPositions[0], fill=self.WINBARWHITE) + winD.ellipse(drawPositions[1], fill=self.WINBARWHITE) + winD.rectangle(drawPositions[2], fill=self.WINBARWHITE) + + elif game["win direction"] == "r": + imageWidth = int(self.PIECEGRIDSIZE[0]*diagonalLength) + imageSize = (imageWidth, self.PIECEGRIDSIZE[1]) + winBar = Image.new("RGBA", imageSize, (0, 0, 0, 0)) + winD = ImageDraw.Draw(winBar) + drawPositions = [ + [ + ( + 0, + 0 + ), ( + self.PIECEGRIDSIZE[0], + self.PIECEGRIDSIZE[1] + ) + ], [ + ( + (self.PIECEGRIDSIZE[0]*(diagonalLength-1)), + 0 + ), ( + self.PIECEGRIDSIZE[0]*diagonalLength, + self.PIECEGRIDSIZE[1] + ) + ], [ + ( + int(self.PIECEGRIDSIZE[0]*0.5), + 0 + ), ( + int(self.PIECEGRIDSIZE[0]*(diagonalLength-0.5)), + self.PIECEGRIDSIZE[1] + ) + ] + ] + winD.ellipse(drawPositions[0], fill=self.WINBARWHITE) + winD.ellipse(drawPositions[1], fill=self.WINBARWHITE) + winD.rectangle(drawPositions[2], fill=self.WINBARWHITE) + winBar = winBar.rotate(-diagonalAngle, expand=1) + startX -= 90 + startY -= 100 + + elif game["win direction"] == "l": + imageWidth = int(self.PIECEGRIDSIZE[0]*diagonalLength) + imageSize = (imageWidth, self.PIECEGRIDSIZE[1]) + winBar = Image.new("RGBA", imageSize, (0, 0, 0, 0)) + winD = ImageDraw.Draw(winBar) + drawPositions = [ + [ + (0, 0), + (self.PIECEGRIDSIZE[0], self.PIECEGRIDSIZE[1]) + ], [ + ( + (self.PIECEGRIDSIZE[0]*(diagonalLength-1)), + 0 + ), ( + self.PIECEGRIDSIZE[0]*diagonalLength, + self.PIECEGRIDSIZE[1] + ) + ], [ + ( + int(self.PIECEGRIDSIZE[0]*0.5), + 0 + ), ( + int(self.PIECEGRIDSIZE[0]*(diagonalLength-0.5)), + self.PIECEGRIDSIZE[1] + ) + ] + ] + winD.ellipse(drawPositions[0], fill=self.WINBARWHITE) + winD.ellipse(drawPositions[1], fill=self.WINBARWHITE) + winD.rectangle(drawPositions[2], fill=self.WINBARWHITE) + winBar = winBar.rotate(diagonalAngle, expand=1) + startX -= self.PIECEGRIDSIZE[0]*3 + 90 + startY -= self.GRIDBORDER + 60 + + mask = winBar.copy() + + winBarImage = Image.new("RGBA", mask.size, color=self.WINBARCOLOR) + background.paste(winBarImage, (startX, startY), mask) + + def _drawFooter(self, d: ImageDraw, game: dict): if game["players"][0] == "Gwendolyn": player1 = "Gwendolyn" else: @@ -477,107 +1030,39 @@ class drawConnectFour(): else: player2 = self.bot.databaseFuncs.getName(game["players"][1]) + exampleHeight = self.HEIGHT - self.BORDER + exampleHeight += (self.BOTTOMBORDER+self.BORDER)//2 - self.TEXTSIZE//2 + textWidth = self.FONT.getsize(player2)[0] + exampleRPadding = self.BORDER+self.TEXTSIZE+textWidth+self.TEXTPADDING + circlePositions = [ + [ + ( + self.BORDER, + exampleHeight + ), ( + self.BORDER+self.TEXTSIZE, + exampleHeight+self.TEXTSIZE + ) + ], [ + ( + self.WIDTH-exampleRPadding, + exampleHeight + ), ( + self.WIDTH-self.BORDER-textWidth-self.TEXTPADDING, + exampleHeight+self.TEXTSIZE + ) + ] + ] + circleParams = { + "outline": self.BOARDOUTLINECOLOR, + "width": 3 + } + d.ellipse(circlePositions[0], fill=self.PLAYER1COLOR, **circleParams) + d.ellipse(circlePositions[1], fill=self.PLAYER2COLOR, **circleParams) - 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") - + textPositions = [ + (self.BORDER+self.TEXTSIZE+self.TEXTPADDING, exampleHeight), + (self.WIDTH-self.BORDER-textWidth, exampleHeight) + ] + d.text(textPositions[0], player1, font=self.FONT, fill=(0, 0, 0)) + d.text(textPositions[1], player2, font=self.FONT, fill=(0, 0, 0)) diff --git a/resources/longStrings.json b/resources/longStrings.json index 188fc7f..c0f1de8 100644 --- a/resources/longStrings.json +++ b/resources/longStrings.json @@ -12,5 +12,7 @@ "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" + "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" } \ No newline at end of file From 431f423b4185984ab8381ae55fbabf73a71acc81 Mon Sep 17 00:00:00 2001 From: NikolajDanger Date: Sun, 18 Apr 2021 21:15:41 +0200 Subject: [PATCH 08/10] :sparkles: Hangman --- funcs/games/hangman.py | 261 +++++++++++++++++++++++-------------- resources/longStrings.json | 5 +- 2 files changed, 169 insertions(+), 97 deletions(-) diff --git a/funcs/games/hangman.py b/funcs/games/hangman.py index f330000..8da9592 100644 --- a/funcs/games/hangman.py +++ b/funcs/games/hangman.py @@ -1,32 +1,59 @@ -import json, urllib, datetime, string, discord -import math, random +import json +import requests +import datetime +import string +import discord +import math +import random +from discord_slash.context import SlashContext from PIL import ImageDraw, Image, ImageFont + class Hangman(): - def __init__(self,bot): + 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=" + self.APIURL = "https://api.wordnik.com/v4/words.json/randomWords?" + apiKey = self.bot.credentials.wordnikKey + self.APIPARAMS = { + "hasDictionaryDef": True, + "minCorpusCount": 5000, + "maxCorpusCount": -1, + "minDictionaryCount": 1, + "maxDictionaryCount": -1, + "minLength": 3, + "maxLength": 11, + "limit": 1, + "api_key": apiKey + } - async def start(self, ctx): + async def start(self, ctx: SlashContext): 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}) + 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 + if game is None: word = "-" while "-" in word or "." in word: - with urllib.request.urlopen(self.APIURL+apiKey) as p: - word = list(json.load(p)[0]["word"].upper()) + response = requests.get(self.APIURL, params=self.APIPARAMS) + word = list(response.json()[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} + 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) @@ -38,17 +65,21 @@ class Hangman(): startedGame = True else: logMessage = "There was already a game going on" - sendMessage = "There's already a Hangman game going on in the channel" + sendMessage = self.bot.longStrings["Hangman going on"] 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)) + boardsPath = "resources/games/hangmanBoards/" + filePath = f"{boardsPath}hangmanBoard{channel}.png" + newImage = await ctx.channel.send(file=discord.File(filePath)) - blankMessage = await ctx.channel.send("_ _") - reactionMessages = {newImage : remainingLetters[:15], blankMessage : remainingLetters[15:]} + blankMessage = await ctx.channel.send("_ _") + reactionMessages = { + newImage: remainingLetters[:15], + blankMessage: remainingLetters[15:] + } oldMessages = f"{newImage.id}\n{blankMessage.id}" @@ -60,7 +91,7 @@ class Hangman(): emoji = chr(ord(letter)+127397) await message.add_reaction(emoji) - async def stop(self, ctx): + async def stop(self, ctx: SlashContext): channel = str(ctx.channel.id) game = self.bot.database["hangman games"].find_one({"_id": channel}) @@ -69,7 +100,7 @@ class Hangman(): 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}) + self.bot.database["hangman games"].delete_one({"_id": channel}) with open(f"resources/games/oldImages/hangman{channel}", "r") as f: messages = f.read().splitlines() @@ -81,11 +112,12 @@ class Hangman(): await ctx.send("Game stopped") - async def guess(self, message, user, guess): + async def guess(self, message: discord.Message, user: str, guess: str): channel = str(message.channel.id) - game = self.bot.database["hangman games"].find_one({"_id":channel}) + hangmanGames = self.bot.database["hangman games"] + game = hangmanGames.find_one({"_id": channel}) - gameExists = (game != None) + gameExists = (game is not None) singleLetter = (len(guess) == 1 and guess.isalpha()) newGuess = (guess not in game["guessed letters"]) validGuess = (gameExists and singleLetter and newGuess) @@ -97,35 +129,40 @@ class Hangman(): 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}}) + updater = {"$set": {f"guessed.{x}": True}} + hangmanGames.update_one({"_id": channel}, updater) if correctGuess == 0: - self.bot.database["hangman games"].update_one({"_id":channel},{"$inc":{"misses":1}}) + updater = {"$inc": {"misses": 1}} + hangmanGames.update_one({"_id": channel}, updater) - self.bot.database["hangman games"].update_one({"_id":channel},{"$push":{"guessed letters":guess}}) + updater = {"$push": {"guessed letters": guess}} + hangmanGames.update_one({"_id": channel}, updater) remainingLetters = list(string.ascii_uppercase) - game = self.bot.database["hangman games"].find_one({"_id":channel}) + game = hangmanGames.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." + sendMessage = "Guessed {}. There was 1 {} in the word." + sendMessage = sendMessage.format(guess, guess) else: - sendMessage = f"Guessed {guess}. There were {correctGuess} {guess}s in the word." + sendMessage = "Guessed {}. There were {} {}s in the word." + sendMessage = sendMessage.format(guess, correctGuess, guess) 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." + hangmanGames.delete_one({"_id": channel}) + sendMessage += self.bot.longStrings["Hangman lost 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" + elif all(game["guessed"]): + hangmanGames.delete_one({"_id": channel}) + self.bot.money.addMoney(user, 15) + sendMessage += self.bot.longStrings["Hangman guessed word"] remainingLetters = [] await message.channel.send(sendMessage) @@ -138,23 +175,28 @@ class Hangman(): 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)) + boardsPath = "resources/games/hangmanBoards/" + filePath = f"{boardsPath}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:]} + blankMessage = await message.channel.send("_ _") + reactionMessages = { + newImage: remainingLetters[:15], + blankMessage: remainingLetters[15:] + } else: blankMessage = "" - reactionMessages = {newImage : remainingLetters} + 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: + oldImagePath = f"resources/games/oldImages/hangman{channel}" + with open(oldImagePath, "w") as f: f.write(oldMessages) for message, letters in reactionMessages.items(): @@ -162,58 +204,85 @@ class Hangman(): emoji = chr(ord(letter)+127397) await message.add_reaction(emoji) + class DrawHangman(): - def __init__(self,bot): + def __init__(self, bot): self.bot = bot self.CIRCLESIZE = 120 self.LINEWIDTH = 12 - self.BODYSIZE =210 + 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 + MANPADDING = self.LINEWIDTH*4 + self.MANX = (self.LIMBSIZE*2)+MANPADDING + self.MANY = (self.CIRCLESIZE+self.BODYSIZE+self.LIMBSIZE)+MANPADDING self.LETTERLINELENGTH = 90 self.LETTERLINEDISTANCE = 30 - self.GALLOWX, self.GALLOWY = 360,600 + 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 + FONTPATH = "resources/fonts/comic-sans-bold.ttf" + self.FONT = ImageFont.truetype(FONTPATH, LETTERSIZE) + self.SMALLFONT = ImageFont.truetype(FONTPATH, TEXTSIZE) - dev = preDev + devAcc - if dev > maxmin: dev = maxmin - elif dev < -maxmin: dev = -maxmin - return dev, devAcc + def calcDeviance(self, preDeviance, preDevianceAccuracy, positionChange, + maxmin, maxAcceleration): + randomDeviance = random.uniform(-positionChange, positionChange) + devianceAccuracy = preDevianceAccuracy + randomDeviance + if devianceAccuracy > maxmin * maxAcceleration: + devianceAccuracy = maxmin * maxAcceleration + elif devianceAccuracy < -maxmin * maxAcceleration: + devianceAccuracy = -maxmin * maxAcceleration + + deviance = preDeviance + devianceAccuracy + if deviance > maxmin: + deviance = maxmin + elif deviance < -maxmin: + deviance = -maxmin + return deviance, devianceAccuracy def badCircle(self): - background = Image.new("RGBA",(self.CIRCLESIZE+(self.LINEWIDTH*3),self.CIRCLESIZE+(self.LINEWIDTH*3)),color=(0,0,0,0)) + circlePadding = (self.LINEWIDTH*3) + imageWidth = self.CIRCLESIZE+circlePadding + imageSize = (imageWidth, imageWidth) + background = Image.new("RGBA", imageSize, color=(0, 0, 0, 0)) - d = ImageDraw.Draw(background,"RGBA") + 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) + devianceX = 0 + devianceY = 0 + devianceAccuracyX = 0 + devianceAccuracyY = 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) + devianceXParams = [ + devianceX, + devianceAccuracyX, + self.LINEWIDTH/100, + self.LINEWIDTH, + 0.03 + ] + devianceYParams = [ + devianceY, + devianceAccuracyY, + self.LINEWIDTH/100, + self.LINEWIDTH, + 0.03 + ] + devianceX, devianceAccuracyX = self.calcDeviance(*devianceXParams) + devianceY, devianceAccuracyY = self.calcDeviance(*devianceYParams) - 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 + x = middle + (math.cos(math.radians(degree+start)) * (self.CIRCLESIZE/2)) - (self.LINEWIDTH/2) + devianceX + y = middle + (math.sin(math.radians(degree+start)) * (self.CIRCLESIZE/2)) - (self.LINEWIDTH/2) + devianceY d.ellipse([(x,y),(x+self.LINEWIDTH,y+self.LINEWIDTH)],fill=(0,0,0,255)) @@ -227,21 +296,21 @@ class DrawHangman(): 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 + devianceX = random.randint(-int(self.LINEWIDTH/3),int(self.LINEWIDTH/3)) + devianceY = 0 + devianceAccuracyX = 0 + devianceAccuracyY = 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) + devianceX, devianceAccuracyX = self.calcDeviance(devianceX,devianceAccuracyX,self.LINEWIDTH/1000,self.LINEWIDTH,0.004) + devianceY, devianceAccuracyY = self.calcDeviance(devianceY,devianceAccuracyY,self.LINEWIDTH/1000,self.LINEWIDTH,0.004) if rotated: - x = self.LINEWIDTH + pixel + devx - y = self.LINEWIDTH + devy + x = self.LINEWIDTH + pixel + devianceX + y = self.LINEWIDTH + devianceY else: - x = self.LINEWIDTH + devx - y = self.LINEWIDTH + pixel + devy + x = self.LINEWIDTH + devianceX + y = self.LINEWIDTH + pixel + devianceY d.ellipse([(x,y),(x+self.LINEWIDTH,y+self.LINEWIDTH)],fill=(0,0,0,255)) @@ -265,33 +334,33 @@ class DrawHangman(): if limb == "ra": limbDrawing = self.badLine(self.LIMBSIZE,True) rotation = random.randint(-45,45) - xpos = int((self.MANX-(self.LINEWIDTH*3))/2) + xPosition = 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 + yPosition = self.CIRCLESIZE+self.ARMPOSITION + rotationCompensation limbDrawing = limbDrawing.rotate(rotation,expand=1) - background.paste(limbDrawing,(xpos,ypos),limbDrawing) + background.paste(limbDrawing,(xPosition,yPosition),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 + xPosition = 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 + yPosition = self.CIRCLESIZE+self.ARMPOSITION + rotationCompensation limbDrawing = limbDrawing.rotate(rotation,expand=1) - background.paste(limbDrawing,(xpos,ypos),limbDrawing) + background.paste(limbDrawing,(xPosition,yPosition),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 + xPosition = int((self.MANX-(self.LINEWIDTH*3))/2)-self.LINEWIDTH + yPosition = self.CIRCLESIZE+self.BODYSIZE-self.LINEWIDTH limbDrawing = limbDrawing.rotate(rotation-45,expand=1) - background.paste(limbDrawing,(xpos,ypos),limbDrawing) + background.paste(limbDrawing,(xPosition,yPosition),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) + xPosition = int((self.MANX-(self.LINEWIDTH*3))/2)-limbDrawing.size[0]+self.LINEWIDTH*3 + yPosition = self.CIRCLESIZE+self.BODYSIZE + background.paste(limbDrawing,(xPosition,yPosition),limbDrawing) return background @@ -329,13 +398,13 @@ class DrawHangman(): 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) + 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) + letterX = int(x*(self.LETTERLINELENGTH+self.LETTERLINEDISTANCE)-(letterWidth/2)+(self.LETTERLINELENGTH*0.5)+(self.LINEWIDTH*2)) + letterLines.paste(letterDrawing,(letterX,0),letterDrawing) return letterLines @@ -343,9 +412,9 @@ class DrawHangman(): 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) + xDistance = abs(i-x) + yDistance = abs(j-y) + dist = math.sqrt(xDistance**2+yDistance**2) if shortestDist > dist: shortestDist = dist return shortestDist diff --git a/resources/longStrings.json b/resources/longStrings.json index c0f1de8..b5d9c11 100644 --- a/resources/longStrings.json +++ b/resources/longStrings.json @@ -14,5 +14,8 @@ "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" + "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" } \ No newline at end of file From 350c805519f401aa1add9781643e8fe0d80e6313 Mon Sep 17 00:00:00 2001 From: Nikolaj Danger Date: Wed, 21 Apr 2021 20:25:14 +0200 Subject: [PATCH 09/10] :Sparkles: Hangman --- funcs/games/hangman.py | 247 +++++++++++++++++++++++------------------ 1 file changed, 141 insertions(+), 106 deletions(-) diff --git a/funcs/games/hangman.py b/funcs/games/hangman.py index 8da9592..b00c727 100644 --- a/funcs/games/hangman.py +++ b/funcs/games/hangman.py @@ -281,29 +281,50 @@ class DrawHangman(): devianceX, devianceAccuracyX = self.calcDeviance(*devianceXParams) devianceY, devianceAccuracyY = self.calcDeviance(*devianceYParams) - x = middle + (math.cos(math.radians(degree+start)) * (self.CIRCLESIZE/2)) - (self.LINEWIDTH/2) + devianceX - y = middle + (math.sin(math.radians(degree+start)) * (self.CIRCLESIZE/2)) - (self.LINEWIDTH/2) + devianceY + radians = math.radians(degree+start) + circleX = (math.cos(radians) * (self.CIRCLESIZE/2)) + circleY = (math.sin(radians) * (self.CIRCLESIZE/2)) - d.ellipse([(x,y),(x+self.LINEWIDTH,y+self.LINEWIDTH)],fill=(0,0,0,255)) + x = middle + circleX - (self.LINEWIDTH/2) + devianceX + y = middle + circleY - (self.LINEWIDTH/2) + devianceY + + circlePosition = [(x, y), (x+self.LINEWIDTH, y+self.LINEWIDTH)] + d.ellipse(circlePosition, fill=(0, 0, 0, 255)) return background - def badLine(self, length, rotated = False): + def badLine(self, length: int, rotated: bool = 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)) + 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") - devianceX = random.randint(-int(self.LINEWIDTH/3),int(self.LINEWIDTH/3)) + d = ImageDraw.Draw(background, "RGBA") + + possibleDeviance = int(self.LINEWIDTH/3) + devianceX = random.randint(-possibleDeviance, possibleDeviance) devianceY = 0 devianceAccuracyX = 0 devianceAccuracyY = 0 for pixel in range(length): - devianceX, devianceAccuracyX = self.calcDeviance(devianceX,devianceAccuracyX,self.LINEWIDTH/1000,self.LINEWIDTH,0.004) - devianceY, devianceAccuracyY = self.calcDeviance(devianceY,devianceAccuracyY,self.LINEWIDTH/1000,self.LINEWIDTH,0.004) + devianceParamsX = [ + devianceX, + devianceAccuracyX, + self.LINEWIDTH/1000, + self.LINEWIDTH, + 0.004 + ] + devianceParamsY = [ + devianceY, + devianceAccuracyY, + self.LINEWIDTH/1000, + self.LINEWIDTH, + 0.004 + ] + devianceX, devianceAccuracyX = self.calcDeviance(*devianceParamsX) + devianceY, devianceAccuracyY = self.calcDeviance(*devianceParamsY) if rotated: x = self.LINEWIDTH + pixel + devianceX @@ -312,165 +333,179 @@ class DrawHangman(): x = self.LINEWIDTH + devianceX y = self.LINEWIDTH + pixel + devianceY - d.ellipse([(x,y),(x+self.LINEWIDTH,y+self.LINEWIDTH)],fill=(0,0,0,255)) + circlePosition = [(x, y), (x+self.LINEWIDTH, y+self.LINEWIDTH)] + d.ellipse(circlePosition, fill=(0, 0, 0, 255)) return background - def drawMan(self, misses): - background = Image.new("RGBA",(self.MANX,self.MANY),color=(0,0,0,0)) + def drawMan(self, misses: int): + manSize = (self.MANX, self.MANY) + background = Image.new("RGBA", manSize, 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) + pasteX = (self.MANX-(self.CIRCLESIZE+(self.LINEWIDTH*3)))//2 + pastePosition = (pasteX, 0) + background.paste(head, pastePosition, head) if misses >= 2: body = self.badLine(self.BODYSIZE) - background.paste(body,(int((self.MANX-(self.LINEWIDTH*3))/2),self.CIRCLESIZE),body) + pasteX = (self.MANX-(self.LINEWIDTH*3))//2 + pastePosition = (pasteX, self.CIRCLESIZE) + background.paste(body, pastePosition, body) if misses >= 3: - limbs = random.sample(["rl","ll","ra","la"],min(misses-2,4)) - else: limbs = [] + 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) - xPosition = int((self.MANX-(self.LINEWIDTH*3))/2) - rotationCompensation = min(-int(math.sin(math.radians(rotation))*(self.LIMBSIZE+(self.LINEWIDTH*3))),0) - yPosition = self.CIRCLESIZE+self.ARMPOSITION + rotationCompensation - limbDrawing = limbDrawing.rotate(rotation,expand=1) - background.paste(limbDrawing,(xPosition,yPosition),limbDrawing) - elif limb == "la": - limbDrawing = self.badLine(self.LIMBSIZE,True) - rotation = random.randint(-45,45) - xPosition = int((self.MANX-(self.LINEWIDTH*3))/2)-self.LIMBSIZE - rotationCompensation = min(int(math.sin(math.radians(rotation))*(self.LIMBSIZE+(self.LINEWIDTH*3))),0) - yPosition = self.CIRCLESIZE+self.ARMPOSITION + rotationCompensation - limbDrawing = limbDrawing.rotate(rotation,expand=1) - background.paste(limbDrawing,(xPosition,yPosition),limbDrawing) - elif limb == "rl": - limbDrawing = self.badLine(self.LIMBSIZE,True) - rotation = random.randint(-15,15) - xPosition = int((self.MANX-(self.LINEWIDTH*3))/2)-self.LINEWIDTH - yPosition = self.CIRCLESIZE+self.BODYSIZE-self.LINEWIDTH - limbDrawing = limbDrawing.rotate(rotation-45,expand=1) - background.paste(limbDrawing,(xPosition,yPosition),limbDrawing) - elif limb == "ll": - limbDrawing = self.badLine(self.LIMBSIZE,True) - rotation = random.randint(-15,15) - limbDrawing = limbDrawing.rotate(rotation+45,expand=1) - xPosition = int((self.MANX-(self.LINEWIDTH*3))/2)-limbDrawing.size[0]+self.LINEWIDTH*3 + limbDrawing = self.badLine(self.LIMBSIZE, True) + xPosition = (self.MANX-(self.LINEWIDTH*3))//2 + + if limb[1] == "a": + rotation = random.randint(-45, 45) + shift = math.sin(math.radians(rotation)) + compensation = int(shift*(self.LIMBSIZE+(self.LINEWIDTH*3))) + limbDrawing = limbDrawing.rotate(rotation, expand=1) + yPosition = self.CIRCLESIZE + self.ARMPOSITION + compensation + if limb == "ra": + compensation = min(-compensation, 0) + else: + xPosition -= self.LIMBSIZE + compensation = min(compensation, 0) + else: + rotation = random.randint(-15, 15) yPosition = self.CIRCLESIZE+self.BODYSIZE - background.paste(limbDrawing,(xPosition,yPosition),limbDrawing) + if limb == "rl": + xPosition -= self.LINEWIDTH + limbDrawing = limbDrawing.rotate(rotation-45, expand=1) + else: + yPosition -= self.LINEWIDTH + xPosition += -limbDrawing.size[0]+self.LINEWIDTH*3 + limbDrawing = limbDrawing.rotate(rotation+45, expand=1) + + pastePosition = (xPosition, yPosition) + background.paste(limbDrawing, pastePosition, limbDrawing) return background - def badText(self, text, big, color=(0,0,0,255)): - if big: font = self.FONT - else: font = self.SMALLFONT + def badText(self, text: str, big: bool, color: tuple = (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") + 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) + 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)) + gallowSize = (self.GALLOWX, self.GALLOWY) + background = Image.new("RGBA", gallowSize, 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) + bottomLine = self.badLine(int(self.GALLOWX * 0.75), True) + bottomLineX = int(self.GALLOWX * 0.125) + bottomLineY = self.GALLOWY-(self.LINEWIDTH*4) + pastePosition = (bottomLineX, bottomLineY) + background.paste(bottomLine, pastePosition, bottomLine) lineTwo = self.badLine(self.GALLOWY-self.LINEWIDTH*6) - background.paste(lineTwo,(int(self.GALLOWX*(0.75*self.GOLDENRATIO)),self.LINEWIDTH*2),lineTwo) + lineTwoX = int(self.GALLOWX*(0.75*self.GOLDENRATIO)) + lineTwoY = self.LINEWIDTH*2 + pastePosition = (lineTwoX, lineTwoY) + background.paste(lineTwo, pastePosition, 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) + topLine = self.badLine(int(self.GALLOWY*0.30), True) + pasteX = int(self.GALLOWX*(0.75*self.GOLDENRATIO))-self.LINEWIDTH + pastePosition = (pasteX, self.LINEWIDTH*3) + background.paste(topLine, pastePosition, 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) + pasteX += int(self.GALLOWY*0.30) + background.paste(lastLine, (pasteX, 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)) + def drawLetterLines(self, word: str, guessed: list, misses: int): + imageWidth = (self.LETTERLINELENGTH+self.LETTERLINEDISTANCE)*len(word) + imageSize = (imageWidth, self.LETTERLINELENGTH+self.LINEWIDTH*3) + letterLines = Image.new("RGBA", imageSize, 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) + line = self.badLine(self.LETTERLINELENGTH, True) + pasteX = x*(self.LETTERLINELENGTH+self.LETTERLINEDISTANCE) + pastePosition = (pasteX, self.LETTERLINELENGTH) + letterLines.paste(line, pastePosition, line) if guessed[x]: - letterDrawing = self.badText(letter,True) + 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) + letterX = x*(self.LETTERLINELENGTH+self.LETTERLINEDISTANCE) + letterX -= (letterWidth//2) + letterX += (self.LETTERLINELENGTH//2)+(self.LINEWIDTH*2) + letterLines.paste(letterDrawing, (letterX, 0), letterDrawing) elif misses == 6: - letterDrawing = self.badText(letter,True,(242,66,54)) + 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) + letterX = x*(self.LETTERLINELENGTH+self.LETTERLINEDISTANCE) + letterX -= (letterWidth//2) + letterX += (self.LETTERLINELENGTH//2)+(self.LINEWIDTH*2) + letterLines.paste(letterDrawing, (letterX, 0), letterDrawing) return letterLines - def shortestDist(self,positions,newPosition): + def shortestDist(self, positions: list, newPosition: tuple): shortestDist = math.inf x, y = newPosition for i, j in positions: xDistance = abs(i-x) yDistance = abs(j-y) dist = math.sqrt(xDistance**2+yDistance**2) - if shortestDist > dist: shortestDist = dist + if shortestDist > dist: + shortestDist = dist return shortestDist - def drawMisses(self,guesses,word): - background = Image.new("RGBA",(600,400),color=(0,0,0,0)) + def drawMisses(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 placed == False: - letter = self.badText(guess,True) + while not placed: + 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) + 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): + def drawImage(self, channel: str): self.bot.log("Drawing hangman image", channel) - game = self.bot.database["hangman games"].find_one({"_id":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)") + gallow = self.drawGallows() + man = self.drawMan(game["misses"]) 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)") + letterLineParams = [game["word"], game["guessed"], game["misses"]] + letterLines = self.drawLetterLines(*letterLineParams) random.seed(game["game ID"]) - try: - misses = self.drawMisses(game["guessed letters"],game["word"]) - except: - self.bot.log("Error drawing misses (error code 1714)") + misses = self.drawMisses(game["guessed letters"], game["word"]) - background.paste(gallow,(100,100),gallow) - background.paste(man,(300,210),man) - background.paste(letterLines,(120,840),letterLines) - background.paste(misses,(600,150),misses) + 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) + missesText = self.badText("MISSES", False) missesTextWidth = missesText.size[0] - background.paste(missesText,(850-int(missesTextWidth/2),50),missesText) + background.paste(missesText, (850-missesTextWidth//2, 50), missesText) - background.save("resources/games/hangmanBoards/hangmanBoard"+channel+".png") + boardPath = f"resources/games/hangmanBoards/hangmanBoard{channel}.png" + background.save(boardPath) From 45511d83887ee0ec1d2482087d83394f9b670877 Mon Sep 17 00:00:00 2001 From: NikolajDanger Date: Thu, 22 Apr 2021 20:36:06 +0200 Subject: [PATCH 10/10] :sparkles: hangman --- funcs/games/hangman.py | 391 ++++++++++++++++++++++++++--------------- 1 file changed, 246 insertions(+), 145 deletions(-) diff --git a/funcs/games/hangman.py b/funcs/games/hangman.py index b00c727..d702688 100644 --- a/funcs/games/hangman.py +++ b/funcs/games/hangman.py @@ -1,22 +1,53 @@ -import json -import requests -import datetime -import string -import discord -import math -import random +""" +Deals with commands and logic for hangman games. -from discord_slash.context import SlashContext -from PIL import ImageDraw, Image, ImageFont +*Classes* +--------- + Hangman() + Deals with the game logic of hangman. + DrawHangman() + Draws the image shown to the player. +""" +import requests # Used for getting the word in Hangman.start() +import datetime # Used for generating the game id +import string # string.ascii_uppercase used +import discord # Used for discord.file and type hints +import math # Used by DrawHangman(), mainly for drawing circles +import random # Used to draw poorly + +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): - self.bot = bot - self.draw = DrawHangman(bot) - self.APIURL = "https://api.wordnik.com/v4/words.json/randomWords?" - apiKey = self.bot.credentials.wordnikKey - self.APIPARAMS = { + """ + Initialize the class. + + *Attributes* + ------------ + draw: DrawHangman + The DrawHangman used to draw the hangman image. + APIURL: 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.__APIURL = "https://api.wordnik.com/v4/words.json/randomWords?" + apiKey = self.__bot.credentials.wordnikKey + self.__APIPARAMS = { "hasDictionaryDef": True, "minCorpusCount": 5000, "maxCorpusCount": -1, @@ -29,20 +60,28 @@ class Hangman(): } async def start(self, ctx: SlashContext): - await self.bot.defer(ctx) + """ + 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}) - userName = self.bot.databaseFuncs.getName(user) + game = self.__bot.database["hangman games"].find_one({"_id": channel}) + userName = self.__bot.databaseFuncs.getName(user) startedGame = False if game is None: word = "-" while "-" in word or "." in word: - response = requests.get(self.APIURL, params=self.APIPARAMS) + response = requests.get(self.__APIURL, params=self.__APIPARAMS) word = list(response.json()[0]["word"].upper()) - self.bot.log("Found the word \""+"".join(word)+"\"") + self.__bot.log("Found the word \""+"".join(word)+"\"") guessed = [False] * len(word) gameID = datetime.datetime.now().strftime("%Y%m%d%H%M%S") newGame = { @@ -54,20 +93,20 @@ class Hangman(): "misses": 0, "guessed": guessed } - self.bot.database["hangman games"].insert_one(newGame) + self.__bot.database["hangman games"].insert_one(newGame) remainingLetters = list(string.ascii_uppercase) - self.draw.drawImage(channel) + 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 = self.bot.longStrings["Hangman going on"] + sendMessage = self.__bot.longStrings["Hangman going on"] - self.bot.log(logMessage) + self.__bot.log(logMessage) await ctx.send(sendMessage) if startedGame: @@ -92,29 +131,49 @@ class Hangman(): 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}) + 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}) + 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") + self.__bot.log("Deleting old message") await oldMessage.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) - hangmanGames = self.bot.database["hangman games"] + hangmanGames = self.__bot.database["hangman games"] game = hangmanGames.find_one({"_id": channel}) gameExists = (game is not None) @@ -123,7 +182,7 @@ class Hangman(): validGuess = (gameExists and singleLetter and newGuess) if validGuess: - self.bot.log("Guessed the letter") + self.__bot.log("Guessed the letter") correctGuess = 0 for x, letter in enumerate(game["word"]): @@ -153,16 +212,16 @@ class Hangman(): sendMessage = "Guessed {}. There were {} {}s in the word." sendMessage = sendMessage.format(guess, correctGuess, guess) - self.draw.drawImage(channel) + self.__draw.drawImage(channel) if game["misses"] == 6: hangmanGames.delete_one({"_id": channel}) - sendMessage += self.bot.longStrings["Hangman lost game"] + sendMessage += self.__bot.longStrings["Hangman lost game"] remainingLetters = [] elif all(game["guessed"]): hangmanGames.delete_one({"_id": channel}) - self.bot.money.addMoney(user, 15) - sendMessage += self.bot.longStrings["Hangman guessed word"] + self.__bot.money.addMoney(user, 15) + sendMessage += self.__bot.longStrings["Hangman guessed word"] remainingLetters = [] await message.channel.send(sendMessage) @@ -172,7 +231,7 @@ class Hangman(): for oldID in oldMessageIDs: oldMessage = await message.channel.fetch_message(int(oldID)) - self.bot.log("Deleting old message") + self.__bot.log("Deleting old message") await oldMessage.delete() boardsPath = "resources/games/hangmanBoards/" @@ -206,34 +265,63 @@ class Hangman(): class DrawHangman(): + """ + Draws the image of the hangman game. + + *Methods* + --------- + drawImage(channel: str) + """ + def __init__(self, bot): - self.bot = bot - self.CIRCLESIZE = 120 - self.LINEWIDTH = 12 + """ + Initialize the class. - self.BODYSIZE = 210 - self.LIMBSIZE = 60 - self.ARMPOSITION = 60 + *Attributes* + ------------ + CIRCLESIZE + LINEWIDTH + BODYSIZE + LIMBSIZE + ARMPOSITION + MANX, MANY + LETTERLINELENGTH + LETTERLINEDISTANCE + GALLOWX, GALLOWY + PHI + FONT + SMALLFONT + """ + self.__bot = bot + self.__CIRCLESIZE = 120 + self.__LINEWIDTH = 12 - MANPADDING = self.LINEWIDTH*4 - self.MANX = (self.LIMBSIZE*2)+MANPADDING - self.MANY = (self.CIRCLESIZE+self.BODYSIZE+self.LIMBSIZE)+MANPADDING + self.__BODYSIZE = 210 + self.__LIMBSIZE = 60 + self.__ARMPOSITION = 60 - self.LETTERLINELENGTH = 90 - self.LETTERLINEDISTANCE = 30 + self.__MANX = (self.__LIMBSIZE*2) + self.__MANY = (self.__CIRCLESIZE+self.__BODYSIZE+self.__LIMBSIZE) + MANPADDING = self.__LINEWIDTH*4 + self.__MANX += MANPADDING + self.__MANY += MANPADDING - self.GALLOWX, self.GALLOWY = 360, 600 - self.GOLDENRATIO = 1-(1 / ((1 + 5 ** 0.5) / 2)) + self.__LETTERLINELENGTH = 90 + self.__LETTERLINEDISTANCE = 30 - LETTERSIZE = 75 - TEXTSIZE = 70 + 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 = "resources/fonts/comic-sans-bold.ttf" - self.FONT = ImageFont.truetype(FONTPATH, LETTERSIZE) - self.SMALLFONT = ImageFont.truetype(FONTPATH, TEXTSIZE) + self.__FONT = ImageFont.truetype(FONTPATH, LETTERSIZE) + self.__SMALLFONT = ImageFont.truetype(FONTPATH, WORDSIZE) - def calcDeviance(self, preDeviance, preDevianceAccuracy, positionChange, - maxmin, maxAcceleration): + def __deviate(self, preDeviance: int, preDevianceAccuracy: int, + positionChange: float, maxmin: int, + maxAcceleration: float): randomDeviance = random.uniform(-positionChange, positionChange) devianceAccuracy = preDevianceAccuracy + randomDeviance if devianceAccuracy > maxmin * maxAcceleration: @@ -248,14 +336,14 @@ class DrawHangman(): deviance = -maxmin return deviance, devianceAccuracy - def badCircle(self): - circlePadding = (self.LINEWIDTH*3) - imageWidth = self.CIRCLESIZE+circlePadding + def __badCircle(self): + circlePadding = (self.__LINEWIDTH*3) + imageWidth = self.__CIRCLESIZE+circlePadding imageSize = (imageWidth, imageWidth) background = Image.new("RGBA", imageSize, color=(0, 0, 0, 0)) d = ImageDraw.Draw(background, "RGBA") - middle = (self.CIRCLESIZE+(self.LINEWIDTH*3))/2 + middle = (self.__CIRCLESIZE+(self.__LINEWIDTH*3))/2 devianceX = 0 devianceY = 0 devianceAccuracyX = 0 @@ -267,42 +355,42 @@ class DrawHangman(): devianceXParams = [ devianceX, devianceAccuracyX, - self.LINEWIDTH/100, - self.LINEWIDTH, + self.__LINEWIDTH/100, + self.__LINEWIDTH, 0.03 ] devianceYParams = [ devianceY, devianceAccuracyY, - self.LINEWIDTH/100, - self.LINEWIDTH, + self.__LINEWIDTH/100, + self.__LINEWIDTH, 0.03 ] - devianceX, devianceAccuracyX = self.calcDeviance(*devianceXParams) - devianceY, devianceAccuracyY = self.calcDeviance(*devianceYParams) + devianceX, devianceAccuracyX = self.__deviate(*devianceXParams) + devianceY, devianceAccuracyY = self.__deviate(*devianceYParams) radians = math.radians(degree+start) - circleX = (math.cos(radians) * (self.CIRCLESIZE/2)) - circleY = (math.sin(radians) * (self.CIRCLESIZE/2)) + circleX = (math.cos(radians) * (self.__CIRCLESIZE/2)) + circleY = (math.sin(radians) * (self.__CIRCLESIZE/2)) - x = middle + circleX - (self.LINEWIDTH/2) + devianceX - y = middle + circleY - (self.LINEWIDTH/2) + devianceY + x = middle + circleX - (self.__LINEWIDTH/2) + devianceX + y = middle + circleY - (self.__LINEWIDTH/2) + devianceY - circlePosition = [(x, y), (x+self.LINEWIDTH, y+self.LINEWIDTH)] + circlePosition = [(x, y), (x+self.__LINEWIDTH, y+self.__LINEWIDTH)] d.ellipse(circlePosition, fill=(0, 0, 0, 255)) return background - def badLine(self, length: int, rotated: bool = False): + def __badLine(self, length: int, rotated: bool = False): if rotated: - w, h = length+self.LINEWIDTH*3, self.LINEWIDTH*3 + w, h = length+self.__LINEWIDTH*3, self.__LINEWIDTH*3 else: - w, h = self.LINEWIDTH*3, length+self.LINEWIDTH*3 + 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") - possibleDeviance = int(self.LINEWIDTH/3) + possibleDeviance = int(self.__LINEWIDTH/3) devianceX = random.randint(-possibleDeviance, possibleDeviance) devianceY = 0 devianceAccuracyX = 0 @@ -312,45 +400,46 @@ class DrawHangman(): devianceParamsX = [ devianceX, devianceAccuracyX, - self.LINEWIDTH/1000, - self.LINEWIDTH, + self.__LINEWIDTH/1000, + self.__LINEWIDTH, 0.004 ] devianceParamsY = [ devianceY, devianceAccuracyY, - self.LINEWIDTH/1000, - self.LINEWIDTH, + self.__LINEWIDTH/1000, + self.__LINEWIDTH, 0.004 ] - devianceX, devianceAccuracyX = self.calcDeviance(*devianceParamsX) - devianceY, devianceAccuracyY = self.calcDeviance(*devianceParamsY) + devianceX, devianceAccuracyX = self.__deviate(*devianceParamsX) + devianceY, devianceAccuracyY = self.__deviate(*devianceParamsY) if rotated: - x = self.LINEWIDTH + pixel + devianceX - y = self.LINEWIDTH + devianceY + x = self.__LINEWIDTH + pixel + devianceX + y = self.__LINEWIDTH + devianceY else: - x = self.LINEWIDTH + devianceX - y = self.LINEWIDTH + pixel + devianceY + x = self.__LINEWIDTH + devianceX + y = self.__LINEWIDTH + pixel + devianceY - circlePosition = [(x, y), (x+self.LINEWIDTH, y+self.LINEWIDTH)] + circlePosition = [(x, y), (x+self.__LINEWIDTH, y+self.__LINEWIDTH)] d.ellipse(circlePosition, fill=(0, 0, 0, 255)) return background - def drawMan(self, misses: int): - manSize = (self.MANX, self.MANY) + def __drawMan(self, misses: int, seed: str): + random.seed(seed) + manSize = (self.__MANX, self.__MANY) background = Image.new("RGBA", manSize, color=(0, 0, 0, 0)) if misses >= 1: - head = self.badCircle() - pasteX = (self.MANX-(self.CIRCLESIZE+(self.LINEWIDTH*3)))//2 + head = self.__badCircle() + pasteX = (self.__MANX-(self.__CIRCLESIZE+(self.__LINEWIDTH*3)))//2 pastePosition = (pasteX, 0) background.paste(head, pastePosition, head) if misses >= 2: - body = self.badLine(self.BODYSIZE) - pasteX = (self.MANX-(self.LINEWIDTH*3))//2 - pastePosition = (pasteX, self.CIRCLESIZE) + body = self.__badLine(self.__BODYSIZE) + pasteX = (self.__MANX-(self.__LINEWIDTH*3))//2 + pastePosition = (pasteX, self.__CIRCLESIZE) background.paste(body, pastePosition, body) if misses >= 3: @@ -358,30 +447,33 @@ class DrawHangman(): else: limbs = [] + random.seed(seed) + for limb in limbs: - limbDrawing = self.badLine(self.LIMBSIZE, True) - xPosition = (self.MANX-(self.LINEWIDTH*3))//2 + limbDrawing = self.__badLine(self.__LIMBSIZE, True) + xPosition = (self.__MANX-(self.__LINEWIDTH*3))//2 if limb[1] == "a": rotation = random.randint(-45, 45) shift = math.sin(math.radians(rotation)) - compensation = int(shift*(self.LIMBSIZE+(self.LINEWIDTH*3))) + lineLength = self.__LIMBSIZE+(self.__LINEWIDTH*3) + compensation = int(shift*lineLength) limbDrawing = limbDrawing.rotate(rotation, expand=1) - yPosition = self.CIRCLESIZE + self.ARMPOSITION + compensation + yPosition = self.__CIRCLESIZE + self.__ARMPOSITION if limb == "ra": compensation = min(-compensation, 0) else: - xPosition -= self.LIMBSIZE + xPosition -= self.__LIMBSIZE compensation = min(compensation, 0) + + yPosition += compensation else: rotation = random.randint(-15, 15) - yPosition = self.CIRCLESIZE+self.BODYSIZE + yPosition = self.__CIRCLESIZE+self.__BODYSIZE-self.__LINEWIDTH if limb == "rl": - xPosition -= self.LINEWIDTH limbDrawing = limbDrawing.rotate(rotation-45, expand=1) else: - yPosition -= self.LINEWIDTH - xPosition += -limbDrawing.size[0]+self.LINEWIDTH*3 + xPosition += -limbDrawing.size[0]+self.__LINEWIDTH*3 limbDrawing = limbDrawing.rotate(rotation+45, expand=1) pastePosition = (xPosition, yPosition) @@ -389,11 +481,11 @@ class DrawHangman(): return background - def badText(self, text: str, big: bool, color: tuple = (0, 0, 0, 255)): + def __badText(self, text: str, big: bool, color: tuple = (0, 0, 0, 255)): if big: - font = self.FONT + font = self.__FONT else: - font = self.SMALLFONT + font = self.__SMALLFONT w, h = font.getsize(text) img = Image.new("RGBA", (w, h), color=(0, 0, 0, 0)) d = ImageDraw.Draw(img, "RGBA") @@ -401,109 +493,118 @@ class DrawHangman(): d.text((0, 0), text, font=font, fill=color) return img - def drawGallows(self): - gallowSize = (self.GALLOWX, self.GALLOWY) + def __drawGallows(self): + gallowSize = (self.__GALLOWX, self.__GALLOWY) background = Image.new("RGBA", gallowSize, color=(0, 0, 0, 0)) - bottomLine = self.badLine(int(self.GALLOWX * 0.75), True) - bottomLineX = int(self.GALLOWX * 0.125) - bottomLineY = self.GALLOWY-(self.LINEWIDTH*4) + bottomLine = self.__badLine(int(self.__GALLOWX * 0.75), True) + bottomLineX = int(self.__GALLOWX * 0.125) + bottomLineY = self.__GALLOWY-(self.__LINEWIDTH*4) pastePosition = (bottomLineX, bottomLineY) background.paste(bottomLine, pastePosition, bottomLine) - lineTwo = self.badLine(self.GALLOWY-self.LINEWIDTH*6) - lineTwoX = int(self.GALLOWX*(0.75*self.GOLDENRATIO)) - lineTwoY = self.LINEWIDTH*2 + lineTwo = self.__badLine(self.__GALLOWY-self.__LINEWIDTH*6) + lineTwoX = int(self.__GALLOWX*(0.75*self.__PHI)) + lineTwoY = self.__LINEWIDTH*2 pastePosition = (lineTwoX, lineTwoY) background.paste(lineTwo, pastePosition, lineTwo) - topLine = self.badLine(int(self.GALLOWY*0.30), True) - pasteX = int(self.GALLOWX*(0.75*self.GOLDENRATIO))-self.LINEWIDTH - pastePosition = (pasteX, self.LINEWIDTH*3) + topLine = self.__badLine(int(self.__GALLOWY*0.30), True) + pasteX = int(self.__GALLOWX*(0.75*self.__PHI))-self.__LINEWIDTH + pastePosition = (pasteX, self.__LINEWIDTH*3) background.paste(topLine, pastePosition, topLine) - lastLine = self.badLine(int(self.GALLOWY*0.125)) - pasteX += int(self.GALLOWY*0.30) - background.paste(lastLine, (pasteX, self.LINEWIDTH*3), lastLine) + lastLine = self.__badLine(int(self.__GALLOWY*0.125)) + pasteX += int(self.__GALLOWY*0.30) + background.paste(lastLine, (pasteX, self.__LINEWIDTH*3), lastLine) return background - def drawLetterLines(self, word: str, guessed: list, misses: int): - imageWidth = (self.LETTERLINELENGTH+self.LETTERLINEDISTANCE)*len(word) - imageSize = (imageWidth, self.LETTERLINELENGTH+self.LINEWIDTH*3) + def __drawLetterLines(self, word: str, guessed: list, misses: int): + letterWidth = self.__LETTERLINELENGTH+self.__LETTERLINEDISTANCE + imageWidth = letterWidth*len(word) + imageSize = (imageWidth, self.__LETTERLINELENGTH+self.__LINEWIDTH*3) letterLines = Image.new("RGBA", imageSize, color=(0, 0, 0, 0)) for x, letter in enumerate(word): - line = self.badLine(self.LETTERLINELENGTH, True) - pasteX = x*(self.LETTERLINELENGTH+self.LETTERLINEDISTANCE) - pastePosition = (pasteX, self.LETTERLINELENGTH) + line = self.__badLine(self.__LETTERLINELENGTH, True) + pasteX = x*(self.__LETTERLINELENGTH+self.__LETTERLINEDISTANCE) + pastePosition = (pasteX, self.__LETTERLINELENGTH) letterLines.paste(line, pastePosition, line) if guessed[x]: - letterDrawing = self.badText(letter, True) - letterWidth = self.FONT.getsize(letter)[0] - letterX = x*(self.LETTERLINELENGTH+self.LETTERLINEDISTANCE) + letterDrawing = self.__badText(letter, True) + letterWidth = self.__FONT.getsize(letter)[0] + letterX = x*(self.__LETTERLINELENGTH+self.__LETTERLINEDISTANCE) letterX -= (letterWidth//2) - letterX += (self.LETTERLINELENGTH//2)+(self.LINEWIDTH*2) + letterX += (self.__LETTERLINELENGTH//2)+(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 = x*(self.LETTERLINELENGTH+self.LETTERLINEDISTANCE) + letterDrawing = self.__badText(letter, True, (242, 66, 54)) + letterWidth = self.__FONT.getsize(letter)[0] + letterX = x*(self.__LETTERLINELENGTH+self.__LETTERLINEDISTANCE) letterX -= (letterWidth//2) - letterX += (self.LETTERLINELENGTH//2)+(self.LINEWIDTH*2) + letterX += (self.__LETTERLINELENGTH//2)+(self.__LINEWIDTH*2) letterLines.paste(letterDrawing, (letterX, 0), letterDrawing) return letterLines - def shortestDist(self, positions: list, newPosition: tuple): - shortestDist = math.inf + def __shortestDist(self, positions: list, newPosition: tuple): + __shortestDist = math.inf x, y = newPosition for i, j in positions: xDistance = abs(i-x) yDistance = abs(j-y) dist = math.sqrt(xDistance**2+yDistance**2) - if shortestDist > dist: - shortestDist = dist - return shortestDist + if __shortestDist > dist: + __shortestDist = dist + return __shortestDist - def drawMisses(self, guesses: list, word: str): + def __drawMisses(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.badText(guess, True) - w, h = self.FONT.getsize(guess) + 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: + 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: str): - self.bot.log("Drawing hangman image", channel) - game = self.bot.database["hangman games"].find_one({"_id": channel}) + """ + 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("resources/paper.jpg") - gallow = self.drawGallows() - man = self.drawMan(game["misses"]) + gallow = self.__drawGallows() + man = self.__drawMan(game["misses"], game["game ID"]) random.seed(game["game ID"]) letterLineParams = [game["word"], game["guessed"], game["misses"]] - letterLines = self.drawLetterLines(*letterLineParams) + letterLines = self.__drawLetterLines(*letterLineParams) random.seed(game["game ID"]) - misses = self.drawMisses(game["guessed letters"], game["word"]) + misses = self.__drawMisses(game["guessed letters"], game["word"]) 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) + missesText = self.__badText("MISSES", False) missesTextWidth = missesText.size[0] background.paste(missesText, (850-missesTextWidth//2, 50), missesText)