""" 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 gwendolyn.utils import replace_multiple class Blackjack(): """ 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 def _blackjack_shuffle(self, channel: str): """ Shuffle an amount of decks equal to self.decks. The shuffled cards are placed in the database as the cards that are used by other blackjack functions. *Parameters* ------------ channel: str The id of the channel where the cards will be used. """ self.bot.log("Shuffling the blackjack deck") with open("gwendolyn/resources/games/deck_of_cards.txt", "r") as f: deck = f.read() allDecks = deck.split("\n") * self.decks random.shuffle(allDecks) blackjack_cards = self.bot.database["blackjack cards"] cards = {"_id": channel} cardUpdater = {"$set": {"_id": channel, "cards": allDecks}} blackjack_cards.update_one(cards, cardUpdater, upsert=True) # Creates hilo file self.bot.log(f"creating hilo doc for {channel}") data = 0 blackjack_hilo = self.bot.database["hilo"] hiloUpdater = {"$set": {"_id": channel, "hilo": data}} blackjack_hilo.update_one({"_id": channel}, hiloUpdater, upsert=True) def _calcHandValue(self, hand: list): """ Calculate the value of a blackjack hand. *Parameters* ------------ hand: list The hand to calculate the value of. Each element in the list is a card represented as a string in the format of "xy" where x is the number of the card (0, k, q, and j for 10s, kings, queens and jacks) and y is the suit (d, c, h or s). *Returns* --------- handValue: int The blackjack value of the hand. """ values = [] values.append(0) for card in hand: cardValue = card[0] cardValue = replace_multiple(cardValue, ["0", "k", "q", "j"], "10") if cardValue == "a": length = len(values) for x in range(length): values.append(values[x] + 11) values[x] += 1 else: for x in range(len(values)): values[x] += int(cardValue) values.sort() handValue = values[0] for value in values: if value <= 21: handValue = value self.bot.log(f"Calculated the value of {hand} to be {handValue}") return handValue 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") blackjack_cards = self.bot.database["blackjack cards"] drawnCard = blackjack_cards.find_one({"_id": channel})["cards"][0] blackjack_cards.update_one({"_id": channel}, {"$pop": {"cards": -1}}) value = self._calcHandValue([drawnCard]) blackjack_hilo = self.bot.database["hilo"] if value <= 6: blackjack_hilo.update_one({"_id": channel}, {"$inc": {"hilo": 1}}) elif value >= 10: blackjack_hilo.update_one({"_id": channel}, {"$inc": {"hilo": -1}}) return drawnCard 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"] blackjack_games = self.bot.database["blackjack games"] if self._calcHandValue(dealerHand) < 17: dealerHand.append(self._drawCard(channel)) dealerUpdater = {"$set": {"dealer hand": dealerHand}} blackjack_games.update_one({"_id": channel}, dealerUpdater) else: done = True if self._calcHandValue(dealerHand) > 21: dealerUpdater = {"$set": {"dealer busted": True}} blackjack_games.update_one({"_id": channel}, dealerUpdater) return done def _blackjack_continue(self, channel: str): """ Continues the blackjack game to the next round. *Parameters* ------------ channel: str The id of the channel the blackjack game is in *Returns* --------- 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}) done = False blackjack_games = self.bot.database["blackjack games"] blackjack_games.update_one({"_id": channel}, {"$inc": {"round": 1}}) allStanding = True preAllStanding = True message = self.bot.long_strings["Blackjack all players standing"] if game["all standing"]: self.bot.log("All are standing") done = self._dealerDraw(channel) message = "The dealer draws a card." blackjack_games.find_one({"_id": channel}) self.bot.log("Testing if all are standing") for user in game["user hands"]: userHand = game["user hands"][user] test_parameters = [userHand, allStanding, preAllStanding, True] standingTest = (self._testIfStanding(*test_parameters)) newUser, allStanding, preAllStanding = standingTest handUpdater = {"$set": {"user hands."+user: newUser}} blackjack_games.update_one({"_id": channel}, handUpdater) if allStanding: gameUpdater = {"$set": {"all standing": True}} blackjack_games.update_one({"_id": channel}, gameUpdater) self.draw.drawImage(channel) if allStanding: 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"] == 0: firstRoundMsg = self.bot.long_strings["Blackjack first round"] else: firstRoundMsg = "" sendMessage = self.bot.long_strings["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 not hand["standing"]: allStanding = False if self._calcHandValue(hand["hand"]) >= 21 or hand["doubled"]: hand["standing"] = True else: preAllStanding = False hand["hit"] = False if topLevel: if hand["split"] >= 1: testHand = hand["other hand"] test_parameters = [testHand, allStanding, preAllStanding, False] standingTest = (self._testIfStanding(*test_parameters)) hand["other hand"], allStanding, preAllStanding = standingTest if hand["split"] >= 2: testHand = hand["third hand"] test_parameters = [testHand, allStanding, preAllStanding, False] standingTest = (self._testIfStanding(*test_parameters)) hand["third hand"], allStanding, preAllStanding = standingTest if hand["split"] >= 3: testHand = hand["fourth hand"] test_parameters = [testHand, allStanding, preAllStanding, False] standingTest = (self._testIfStanding(*test_parameters)) hand["fourth hand"], allStanding, preAllStanding = standingTest return hand, allStanding, preAllStanding def _blackjack_finish(self, channel: str): """ Generate the winnings message after the blackjack game ends. *Parameters* ------------ channel: str The id of the channel the game is in. *Returns* --------- 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 user_name = self.bot.database_funcs.get_name(user) if winnings < 0: if winnings == -1: finalWinnings += f"{user_name} lost 1 GwendoBuck {reason}\n" else: moneyLost = -1 * winnings winningText = f"{user_name} lost {moneyLost} GwendoBucks" winningText += f" {reason}\n" finalWinnings += winningText else: if winnings == 1: finalWinnings += f"{user_name} won 1 GwendoBuck {reason}\n" else: winningText = f"{user_name} 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 _blackjack_loop(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)) old_imagePath = f"gwendolyn/resources/games/old_images/blackjack{channel.id}" with open(old_imagePath, "r") as f: old_image = await channel.fetch_message(int(f.read())) continueData = (self._blackjack_continue(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 old_image.delete() tablesPath = "gwendolyn/resources/games/blackjack_tables/" file_path = f"{tablesPath}blackjack_table{channel.id}.png" old_image = await channel.send(file=discord.File(file_path)) with open(old_imagePath, "w") as f: f.write(str(old_image.id)) if allStanding: await asyncio.sleep(5) else: await asyncio.sleep(120) blackjack_games = self.bot.database["blackjack games"] game = blackjack_games.find_one({"_id": str(channel.id)}) if game is None: rightRound = False else: realRound = game["round"] or -1 realID = game["gameID"] or -1 rightRound = gameRound == realRound and gameID == realID if rightRound: if not gamedone: log_message = f"Loop {gameRound} calling self._blackjack_loop()" self.bot.log(log_message, str(channel.id)) await self._blackjack_loop(channel, gameRound+1, gameID) else: new_message = self._blackjack_finish(str(channel.id)) await channel.send(new_message) else: log_message = f"Ending loop on round {gameRound}" self.bot.log(log_message, 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 blackjack_games = self.bot.database["blackjack games"] game = blackjack_games.find_one({"_id": channel}) if user in game["user hands"]: userHands = game["user hands"][user] hand, handNumber = self._getHandNumber(userHands, handNumber) if hand is None: log_message = "They didn't specify a hand" sendMessage = "You need to specify a hand" elif game["round"] <= 0: log_message = "They tried to hit on the 0th round" sendMessage = "You can't hit before you see your cards" elif hand["hit"]: log_message = "They've already hit this round" sendMessage = "You've already hit this round" elif hand["standing"]: log_message = "They're already standing" sendMessage = "You can't hit when you're standing" else: hand["hand"].append(self._drawCard(channel)) hand["hit"] = True handValue = self._calcHandValue(hand["hand"]) if handValue > 21: hand["busted"] = True if handNumber == 2: 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: handPath = f"user hands.{user}" gameUpdater = {"$set": {handPath: hand}} blackjack_games.update_one({"_id": channel}, gameUpdater) game = blackjack_games.find_one({"_id": channel}) roundDone = self._isRoundDone(game) sendMessage = f"{ctx.author.display_name} hit" log_message = "They succeeded" else: log_message = "They tried to hit without being in the game" sendMessage = "You have to enter the game before you can hit" await ctx.send(sendMessage) self.bot.log(log_message) if roundDone: gameID = game["gameID"] self.bot.log("Hit calling self._blackjack_loop()", channel) await self._blackjack_loop(ctx.channel, game["round"]+1, gameID) 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 blackjack_games = self.bot.database["blackjack games"] game = blackjack_games.find_one({"_id": channel}) if user in game["user hands"]: handParams = [game["user hands"][user], handNumber] hand, handNumber = self._getHandNumber(*handParams) if hand is None: log_message = "They didn't specify a hand" sendMessage = "You need to specify a hand" elif game["round"] <= 0: log_message = "They tried to hit on the 0th round" sendMessage = "You can't hit before you see your cards" elif hand["hit"]: log_message = "They've already hit this round" sendMessage = "You've already hit this round" elif hand["standing"]: log_message = "They're already standing" sendMessage = "You can't hit when you're standing" elif len(hand["hand"]) != 2: log_message = "They tried to double after round 1" sendMessage = "You can only double on the first round" elif self.bot.money.checkBalance(user) < hand["bet"]: log_message = "They tried to double without being in the game" sendMessage = "You can't double when you're not in the game" else: bet = hand["bet"] self.bot.money.addMoney(user, -1 * bet) hand["hand"].append(self._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: handPath = f"user hands.{user}" gameUpdater = {"$set": {handPath: hand}} blackjack_games.update_one({"_id": channel}, gameUpdater) game = blackjack_games.find_one({"_id": channel}) roundDone = self._isRoundDone(game) sendMessage = self.bot.long_strings["Blackjack double"] user_name = self.bot.database_funcs.get_name(user) sendMessage = sendMessage.format(bet, user_name) log_message = "They succeeded" else: log_message = "They tried to double without being in the game" sendMessage = "You can't double when you're not in the game" await ctx.send(sendMessage) self.bot.log(log_message) if roundDone: gameID = game["gameID"] self.bot.log("Double calling self._blackjack_loop()", channel) await self._blackjack_loop(ctx.channel, game["round"]+1, gameID) 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 blackjack_games = self.bot.database["blackjack games"] game = blackjack_games.find_one({"_id": channel}) if game is not None and user in game["user hands"]: handParams = [game["user hands"][user], handNumber] hand, handNumber = self._getHandNumber(*handParams) if hand is None: sendMessage = "You need to specify which hand" log_message = "They didn't specify a hand" elif game["round"] <= 0: sendMessage = "You can't stand before you see your cards" log_message = "They tried to stand on round 0" elif hand["hit"]: sendMessage = "You've already hit this round" log_message = "They'd already hit this round" elif hand["standing"]: sendMessage = "You're already standing" log_message = "They're already standing" else: hand["standing"] = 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: handPath = f"user hands.{user}" gameUpdater = {"$set": {handPath: hand}} blackjack_games.update_one({"_id": channel}, gameUpdater) game = blackjack_games.find_one({"_id": channel}) roundDone = self._isRoundDone(game) sendMessage = f"{ctx.author.display_name} is standing" log_message = "They succeeded" else: log_message = "They tried to stand without being in the game" sendMessage = "You have to enter the game before you can stand" await ctx.send(sendMessage) self.bot.log(log_message) if roundDone: gameID = game["gameID"] self.bot.log("Stand calling self._blackjack_loop()", channel) await self._blackjack_loop(ctx.channel, game["round"]+1, gameID) 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 blackjack_games = self.bot.database["blackjack games"] game = blackjack_games.find_one({"_id": channel}) if game["user hands"][user]["split"] == 0: hand = game["user hands"][user] newHand = game["user hands"][user]["other hand"] handNumber = 0 otherHand = 2 else: if handNumber == 1: hand = game["user hands"][user] elif handNumber == 2: hand = game["user hands"][user]["other hand"] elif handNumber == 3: hand = game["user hands"][user]["third hand"] else: handNumberError = True if game["user hands"][user]["split"] == 1: newHand = game["user hands"][user]["third hand"] otherHand = 3 else: newHand = game["user hands"][user]["fourth hand"] otherHand = 4 if handNumberError: log_message = "They didn't specify a hand" sendMessage = "You have to specify the hand you're hitting with" elif game["round"] == 0: log_message = "They tried to split on round 0" sendMessage = "You can't split before you see your cards" elif game["user hands"][user]["split"] > 3: log_message = "They tried to split more than three times" sendMessage = "You can only split 3 times" elif hand["hit"]: log_message = "They've already hit" sendMessage = "You've already hit or split this hand." elif hand["standing"]: log_message = "They're already standing" sendMessage = "You're already standing" elif len(hand["hand"]) != 2: log_message = "They tried to split after the first round" sendMessage = "You can only split on the first round" else: firstCard = self._calcHandValue([hand["hand"][0]]) secondCard = self._calcHandValue([hand["hand"][1]]) if firstCard != secondCard: log_message = "They tried to split two different cards" sendMessage = self.bot.long_strings["Blackjack different cards"] else: bet = hand["bet"] if self.bot.money.checkBalance(user) < bet: log_message = "They didn't have enough GwendoBucks" sendMessage = "You don't have enough GwendoBucks" else: self.bot.money.addMoney(user, -1 * bet) hand["hit"] = True newHand["hit"] = True newHand = { "hand": [], "bet": 0, "standing": False, "busted": False, "blackjack": False, "hit": True, "doubled": False } newHand["bet"] = hand["bet"] newHand["hand"].append(hand["hand"].pop(1)) newHand["hand"].append(self._drawCard(channel)) hand["hand"].append(self._drawCard(channel)) hand["hit"] = True handValue = self._calcHandValue(hand["hand"]) otherHandValue = self._calcHandValue(newHand["hand"]) if handValue > 21: hand["busted"] = True elif handValue == 21: hand["blackjack"] = True if otherHandValue > 21: newHand["busted"] = True elif otherHandValue == 21: newHand["blackjack"] = True if handNumber == 2: handPath = f"user hands.{user}.other hand" elif handNumber == 3: handPath = f"user hands.{user}.third hand" else: handPath = f"user hands.{user}" gameUpdater = {"$set": {handPath: hand}} blackjack_games.update_one({"_id": channel}, gameUpdater) if otherHand == 3: otherHandPath = f"user hands.{user}.third hand" elif otherHand == 4: otherHandPath = f"user hands.{user}.fourth hand" else: otherHandPath = f"user hands.{user}.other hand" gameUpdater = {"$set": {otherHandPath: newHand}} blackjack_games.update_one({"_id": channel}, gameUpdater) splitUpdater = {"$inc": {"user hands."+user+".split": 1}} blackjack_games.update_one({"_id": channel}, splitUpdater) game = blackjack_games.find_one({"_id": channel}) roundDone = self._isRoundDone(game) sendMessage = self.bot.long_strings["Blackjack split"] user_name = self.bot.database_funcs.get_name(user) sendMessage = sendMessage.format(user_name) log_message = "They succeeded" await ctx.send(sendMessage) self.bot.log(log_message) if roundDone: gameID = game["gameID"] self.bot.log("Stand calling self._blackjack_loop()", channel) await self._blackjack_loop(ctx.channel, game["round"]+1, gameID) 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}) user_name = self.bot.database_funcs.get_name(user) self.bot.log(f"{user_name} is trying to join the Blackjack game") if game is None: sendMessage = "There is no game going on in this channel" log_message = sendMessage elif user in game["user hands"]: sendMessage = "You're already in the game!" log_message = "They're already in the game" elif len(game["user hands"]) >= 5: sendMessage = "There can't be more than 5 players in a game" log_message = "There were already 5 players in the game" elif game["round"] != 0: sendMessage = "The table is no longer taking bets" log_message = "They tried to join after the game begun" elif bet < 0: sendMessage = "You can't bet a negative amount" log_message = "They tried to bet a negative amount" elif self.bot.money.checkBalance(user) < bet: sendMessage = "You don't have enough GwendoBucks" log_message = "They didn't have enough GwendoBucks" else: self.bot.money.addMoney(user, -1 * bet) playerHand = [self._drawCard(channel) for _ in range(2)] handValue = self._calcHandValue(playerHand) if handValue == 21: blackjack_hand = True else: blackjack_hand = False newHand = { "hand": playerHand, "bet": bet, "standing": False, "busted": False, "blackjack": blackjack_hand, "hit": True, "doubled": False, "split": 0, "other hand": {}, "third hand": {}, "fourth hand": {} } function = {"$set": {f"user hands.{user}": newHand}} collection.update_one({"_id": channel}, function) enterGameText = "entered the game with a bet of" betText = f"{bet} GwendoBucks" sendMessage = f"{user_name} {enterGameText} {betText}" log_message = sendMessage self.bot.log(log_message) await ctx.send(sendMessage) async def start(self, ctx: discord_slash.context.SlashContext): """ Start a blackjack game. *Parameters* ------------ ctx: discord_slash.context.SlashContext The context of the command. """ await self.bot.defer(ctx) channel = str(ctx.channel_id) blackjack_minCards = 50 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 is not None: cardsLeft = len(cards["cards"]) # Shuffles if not enough cards if cardsLeft < blackjack_minCards: self._blackjack_shuffle(channel) self.bot.log("Shuffling the blackjack deck...", channel) await ctx.channel.send("Shuffling the deck...") game = self.bot.database["blackjack games"].find_one({"_id": channel}) self.bot.log("Trying to start a blackjack game in "+channel) gameStarted = False if game is None: dealerHand = [self._drawCard(channel), self._drawCard(channel)] gameID = datetime.datetime.now().strftime('%Y%m%d%H%M%S') newGame = { "_id": channel, "dealer hand": dealerHand, "dealer busted": False, "dealer blackjack": False, "user hands": {}, "all standing": False, "round": 0, "gameID": gameID } if self._calcHandValue(dealerHand) == 21: newGame["dealer blackjack"] = True self.bot.database["blackjack games"].insert_one(newGame) tableImagesPath = "gwendolyn/resources/games/blackjack_tables/" emptyTableImagePath = f"gwendolyn/resources/games/blackjack_table.png" newTableImagePath = f"{tableImagesPath}blackjack_table{channel}.png" copyfile(emptyTableImagePath, newTableImagePath) gameStarted = True if gameStarted: sendMessage = self.bot.long_strings["Blackjack started"] await ctx.channel.send(sendMessage) tableImagesPath = "gwendolyn/resources/games/blackjack_tables/" file_path = f"{tableImagesPath}blackjack_table{channel}.png" old_image = await ctx.channel.send(file=discord.File(file_path)) with open("gwendolyn/resources/games/old_images/blackjack"+channel, "w") as f: f.write(str(old_image.id)) await asyncio.sleep(30) gamedone = False blackjack_games = self.bot.database["blackjack games"] game = blackjack_games.find_one({"_id": channel}) if len(game["user hands"]) == 0: gamedone = True sendMessage = "No one entered the game. Ending the game." await ctx.channel.send(sendMessage) gameID = game["gameID"] # Loop of game rounds if not gamedone: self.bot.log("start() calling _blackjack_loop()", channel) await self._blackjack_loop(ctx.channel, 1, gameID) else: new_message = self._blackjack_finish(channel) await ctx.channel.send(new_message) else: sendMessage = self.bot.long_strings["Blackjack going on"] await ctx.channel.send(sendMessage) self.bot.log("There was already a game going on") async def hilo(self, ctx: discord_slash.context.SlashContext): """ Get the hilo of the blackjack game. *Parameters* ------------ ctx: discord_slash.context.SlashContext The context of the command. """ channel = ctx.channel_id data = self.bot.database["hilo"].find_one({"_id": str(channel)}) if data is not None: hilo = str(data["hilo"]) else: hilo = "0" await ctx.send(f"Hi-lo value: {hilo}", hidden=True) async def shuffle(self, ctx: discord_slash.context.SlashContext): """ Shuffle the cards used for blackjack. *Parameters* ------------ ctx: discord_slash.context.SlashContext The context of the command. """ channel = ctx.channel_id self._blackjack_shuffle(str(channel)) self.bot.log("Shuffling the blackjack deck...", str(channel)) await ctx.send("Shuffling the deck...") async def cards(self, ctx: discord_slash.context.SlashContext): """ Get how many cards are left for blackjack. *Parameters* ------------ ctx: discord_slash.context.SlashContext The context of the command. """ channel = ctx.channel_id cardsLeft = 0 blackjack_games = self.bot.database["blackjack cards"] cards = blackjack_games.find_one({"_id": str(channel)}) if cards is not None: cardsLeft = len(cards["cards"]) decksLeft = round(cardsLeft/52, 1) sendMessage = f"Cards left:\n{cardsLeft} cards, {decksLeft} decks" await ctx.send(sendMessage, hidden=True) class DrawBlackjack(): """ 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 = [2, 1, 3, 0, 4] def drawImage(self, channel: str): """ Draw the table image. *Parameters* ------------ channel: str The id of the channel the game is in. """ self.bot.log("Drawing blackjack table", channel) game = self.bot.database["blackjack games"].find_one({"_id": channel}) font = ImageFont.truetype('gwendolyn/resources/fonts/futura-bold.ttf', 50) smallFont = ImageFont.truetype('gwendolyn/resources/fonts/futura-bold.ttf', 40) self.SMALLBORDER = int(self.BORDER/3.5) table = Image.open("gwendolyn/resources/games/blackjack_table.png") textImage = ImageDraw.Draw(table) hands = game["user hands"] dealerBusted = game["dealer busted"] dealerBlackjack = game["dealer blackjack"] if not game["all standing"]: handParams = [ game["dealer hand"], True, False, False ] else: handParams = [ game["dealer hand"], False, dealerBusted, dealerBlackjack ] 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.database_funcs.get_name(key) handParams = [ value["hand"], False, value["busted"], value["blackjack"] ] userHand = self._drawHand(*handParams) positionX = 32-self.SMALLBORDER+(384*self.PLACEMENT[x]) 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: 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") tableImagesPath = "gwendolyn/resources/games/blackjack_tables/" tableImagePath = f"{tableImagesPath}blackjack_table{channel}.png" table.save(tableImagePath) return 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('gwendolyn/resources/fonts/futura-bold.ttf', 200) font2 = ImageFont.truetype('gwendolyn/resources/fonts/futura-bold.ttf', 120) length = len(hand) 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("gwendolyn/resources/games/cards/"+hand[0].upper()+".png") cardPosition = (self.BORDER+self.PLACEMENT[0], cardY) background.paste(img, cardPosition, img) img = Image.open("gwendolyn/resources/games/cards/red_back.png") cardPosition = (125+self.BORDER+self.PLACEMENT[0], cardY) background.paste(img, cardPosition, img) else: for x in range(length): cardPath = f"gwendolyn/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) if busted: 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 = 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)