From 5330a51fe0104c32eb764313b600c7199151639e Mon Sep 17 00:00:00 2001 From: NikolajDanger Date: Tue, 24 Aug 2021 22:28:05 +0200 Subject: [PATCH] :broom: cleaned up blackjack.py --- .gitignore | 1 + gwendolyn/cogs/game_cog.py | 2 +- gwendolyn/funcs/games/blackjack.py | 1430 ++++++----------- gwendolyn/funcs/games/game_base.py | 219 +++ gwendolyn/gwendolyn_client.py | 2 +- .../blackjack.png} | Bin .../default_images/hangman.jpg} | Bin gwendolyn/utils/util_functions.py | 2 +- 8 files changed, 679 insertions(+), 977 deletions(-) create mode 100644 gwendolyn/funcs/games/game_base.py rename gwendolyn/resources/games/{blackjack_table.png => default_images/blackjack.png} (100%) rename gwendolyn/resources/{paper.jpg => games/default_images/hangman.jpg} (100%) diff --git a/.gitignore b/.gitignore index affb8ee..65a36ed 100644 --- a/.gitignore +++ b/.gitignore @@ -157,6 +157,7 @@ gwendolyn/resources/star_wars/destinyPoints.txt gwendolyn/resources/plex/ gwendolyn/resources/games/hilo/ gwendolyn/resources/games/blackjack_tables/ +gwendolyn/resources/games/images/ gwendolyn/resources/games/old_images/ gwendolyn/resources/games/connect_four_boards/ gwendolyn/resources/games/hex_boards/ diff --git a/gwendolyn/cogs/game_cog.py b/gwendolyn/cogs/game_cog.py index 4388dd4..36b0e97 100644 --- a/gwendolyn/cogs/game_cog.py +++ b/gwendolyn/cogs/game_cog.py @@ -53,7 +53,7 @@ class BlackjackCog(commands.Cog): await self.bot.games.blackjack.enter_game(ctx, bet) @cog_ext.cog_subcommand(**params["blackjack_stand"]) - async def blackjack_stand(self, ctx, hand=""): + async def blackjack_stand(self, ctx, hand=0): """Stand on your hand in blackjack.""" await self.bot.games.blackjack.stand(ctx, hand) diff --git a/gwendolyn/funcs/games/blackjack.py b/gwendolyn/funcs/games/blackjack.py index 57c053a..22bc931 100644 --- a/gwendolyn/funcs/games/blackjack.py +++ b/gwendolyn/funcs/games/blackjack.py @@ -8,50 +8,104 @@ Runs commands, game logic and imaging for blackjack games. DrawBlackjack Draws images of the blackjack table. """ -import random # Used to shuffle the blackjack cards import math # Used for flooring decimal numbers import datetime # Used to generate the game id import asyncio # Used for sleeping -from shutil import copyfile -import discord # Used for discord.file -import discord_slash # Used for typehints -from PIL import Image, ImageDraw, ImageFont +from discord_slash.context import SlashContext # Used for typehints +from PIL import Image from gwendolyn.utils import replace_multiple +from .game_base import CardGame, CardDrawer -class Blackjack(): +WHITE = (255, 255, 255) +BLACK = (0, 0, 0) +RED = (255, 50, 50) +GOLD = (155, 123, 0) + +def _is_round_done(game: dict): + """ + Find out if the round is done. + + *Parameters* + ------------ + game: dict + The game to check. + + *Returns* + --------- + round_done: bool + Whether the round is done. + """ + round_done = True + + for user_hands in game["user hands"].values(): + for hand in user_hands: + if (not hand["hit"]) and (not hand["standing"]): + round_done = False + break + + return round_done + +class Blackjack(CardGame): """ Deals with blackjack commands and gameplay logic. *Methods* --------- - hit(ctx: discord_slash.context.SlashContext, - hand_number: int = 0) - double(ctx: discord_slash.context.SlashContext, - hand_number: int = 0) - stand(ctx: discord_slash.context.SlashContext, - hand_number: int = 0) - split(ctx: discord_slash.context.SlashContext, - hand_number: int = 0) - enter_game(ctx: discord_slash.context.SlashContext, bet: int) - start(ctx: discord_slash.context.SlashContext) - hilo(ctx: discord_slash.context.SlashContext) - shuffle(ctx: discord_slash.context.SlashContext) - cards(ctx: discord_slash.context.SlashContext) + hit(ctx: SlashContext, hand_number: int = 0) + double(ctx: SlashContext, hand_number: int = 0) + stand(ctx: SlashContext, hand_number: int = 0) + split(ctx: SlashContext, hand_number: int = 0) + enter_game(ctx: SlashContext, bet: int) + start(ctx: SlashContext) + hilo(ctx: SlashContext) + shuffle(ctx: SlashContext) + cards(ctx: SlashContext) """ def __init__(self, bot): """Initialize the class.""" - self.bot = bot - self.draw = DrawBlackjack(bot) - self.decks = 4 - self.long_strings = self.bot.long_strings + super().__init__(bot) + self.decks_used = 4 + self.game_name = "blackjack" + self.draw = DrawBlackjack(bot, self) - def _blackjack_shuffle(self, channel: str): + async def _test_valid_command(self, game: dict, user: str, + hand_number: int, ctx: SlashContext): + valid_command = False + + if game is None: + self.bot.log("There's no game going on") + await ctx.send("There's no game going on") + elif user not in game["user hands"]: + self.bot.log("They tried something without being in the game") + await ctx.send("You have to enter the game before you can hit") + + elif len(game["user hands"][user]) > 1 and hand_number == 0: + self.bot.log("They didn't specify a hand") + await ctx.send("You need to specify a hand") + elif len(game["user hands"][user]) < hand_number: + self.bot.log("They tried something with a hand they don't have") + await ctx.send("You don't have that many hands") + elif game["round"] <= 0: + self.bot.log("They tried to do something on the 0th round") + await ctx.send("You haven't seen your cards yet!") + elif game["user hands"][user][max(hand_number-1,0)]["hit"]: + self.bot.log("They've already hit this round") + await ctx.send("You've already hit this round") + elif game["user hands"][user][max(hand_number-1,0)]["standing"]: + self.bot.log("They're already standing") + await ctx.send("You're already standing") + else: + valid_command = True + + return valid_command + + def _shuffle_cards(self, channel: str): """ - Shuffle an amount of decks equal to self.decks. + Shuffle an amount of decks equal to self.decks_used. The shuffled cards are placed in the database as the cards that are used by other blackjack functions. @@ -61,26 +115,12 @@ class Blackjack(): channel: str The id of the channel where the cards will be used. """ - self.bot.log("Shuffling the blackjack deck") - - deck_path = "gwendolyn/resources/games/deck_of_cards.txt" - with open(deck_path, "r") as file_pointer: - deck = file_pointer.read() - - all_decks = deck.split("\n") * self.decks - random.shuffle(all_decks) - - blackjack_cards = self.bot.database["blackjack cards"] - cards = {"_id": channel} - card_updater = {"$set": {"_id": channel, "cards": all_decks}} - blackjack_cards.update_one(cards, card_updater, upsert=True) + super()._shuffle_cards(channel) # Creates hilo file self.bot.log(f"creating hilo doc for {channel}") - data = 0 - blackjack_hilo = self.bot.database["hilo"] - hilo_updater = {"$set": {"_id": channel, "hilo": data}} - blackjack_hilo.update_one({"_id": channel}, hilo_updater, upsert=True) + hilo_updater = {"$set": {"_id": channel, "hilo": 0}} + self._update_document(channel, hilo_updater, "hilo") def _calc_hand_value(self, hand: list): """ @@ -105,46 +145,25 @@ class Blackjack(): for card in hand: card_value = replace_multiple(card[0], ["0", "k", "q", "j"], "10") if card_value == "a": - length = len(values) - for i in range(length): - values.append(values[i] + 11) - values[i] += 1 + values = [i + 1 for i in values] + [i + 11 for i in values] else: - values = [int(i)+int(card_value) for i in values] + values = [i+int(card_value) for i in values] - values.sort() - - hand_value = values[0] - for value in values: - if value <= 21: - hand_value = value + valid_values = [i for i in values if i <= 21] + hand_value = max(valid_values) if valid_values else min(values) self.bot.log(f"Calculated the value of {hand} to be {hand_value}") return hand_value def _draw_card(self, channel: str): - """ - Draw a card from the stack. - - *Parameters* - ------------ - channel: str - The id of the channel the card is drawn in. - """ - self.bot.log("drawing a card") - - blackjack_cards = self.bot.database["blackjack cards"] - drawn_card = blackjack_cards.find_one({"_id": channel})["cards"][0] - blackjack_cards.update_one({"_id": channel}, {"$pop": {"cards": -1}}) + drawn_card = super()._draw_card(channel) value = self._calc_hand_value([drawn_card]) - blackjack_hilo = self.bot.database["hilo"] - if value <= 6: - blackjack_hilo.update_one({"_id": channel}, {"$inc": {"hilo": 1}}) + self._update_document(channel, {"$inc": {"hilo": 1}}, "hilo") elif value >= 10: - blackjack_hilo.update_one({"_id": channel}, {"$inc": {"hilo": -1}}) + self._update_document(channel, {"$inc": {"hilo": -1}}, "hilo") return drawn_card @@ -162,23 +181,20 @@ class Blackjack(): done: bool Whether the dealer is done drawing cards. """ - game = self.bot.database["blackjack games"].find_one({"_id": channel}) + game = self.access_document(channel) done = False dealer_hand = game["dealer hand"] - blackjack_games = self.bot.database["blackjack games"] - if self._calc_hand_value(dealer_hand) < 17: dealer_hand.append(self._draw_card(channel)) dealer_updater = {"$set": {"dealer hand": dealer_hand}} - blackjack_games.update_one({"_id": channel}, dealer_updater) + self._update_document(channel, dealer_updater) else: done = True if self._calc_hand_value(dealer_hand) > 21: - dealer_updater = {"$set": {"dealer busted": True}} - blackjack_games.update_one({"_id": channel}, dealer_updater) + self._update_document(channel, {"$set": {"dealer busted": True}}) return done @@ -201,12 +217,11 @@ class Blackjack(): If the game has finished. """ self.bot.log("Continuing blackjack game") - game = self.bot.database["blackjack games"].find_one({"_id": channel}) + game = self.access_document(channel) done = False - blackjack_games = self.bot.database["blackjack games"] - blackjack_games.update_one({"_id": channel}, {"$inc": {"round": 1}}) + self._update_document(channel, {"$inc": {"round": 1}}) all_standing = True pre_all_standing = True @@ -218,43 +233,36 @@ class Blackjack(): done = self._dealer_draw(channel) message = "The dealer draws a card." - blackjack_games.find_one({"_id": channel}) - self.bot.log("Testing if all are standing") - for user in game["user hands"]: - user_hand = game["user hands"][user] - test_parameters = [user_hand, all_standing, pre_all_standing, True] - standing_test = (self._test_if_standing(*test_parameters)) - new_user, all_standing, pre_all_standing = standing_test - hand_updater = {"$set": {"user hands."+user: new_user}} - blackjack_games.update_one({"_id": channel}, hand_updater) + for user, user_hands in game["user hands"].items(): + standing_test = (self._test_if_standing(user_hands)) + new_hands, all_standing, pre_all_standing = standing_test + hand_updater = {"$set": {"user hands."+user: new_hands}} + self._update_document(channel, hand_updater) if all_standing: - game_updater = {"$set": {"all standing": True}} - blackjack_games.update_one({"_id": channel}, game_updater) - - self.draw.draw_image(channel) + self._update_document(channel, {"$set": {"all standing": True}}) if all_standing: - if not done: - return message, True, done - else: - return "The dealer is done drawing cards", True, done - elif pre_all_standing: + if done: + message = "The dealer is done drawing cards" + + return message, True, done + + if pre_all_standing: return "", True, done + + send_message = self.long_strings["Blackjack commands"] + + if game["round"] == 0: + send_message = send_message.format( + self.long_strings["Blackjack first round"]) else: - send_message = self.long_strings["Blackjack commands"] + send_message = send_message.format("") - if game["round"] == 0: - send_message = send_message.format( - self.long_strings["Blackjack first round"]) - else: - send_message = send_message.format("") + return send_message, False, done - return send_message, False, done - - def _test_if_standing(self, hand: dict, all_standing: bool, - pre_all_standing: bool, top_level: bool): + def _test_if_standing(self, user_hands: list): """ Test if a player is standing on all their hands. @@ -263,16 +271,7 @@ class Blackjack(): *Parameters* ------------ hand: dict - The hand to test and reset. - all_standing: bool - Is set to True at the top level. If it's false, the - player is not standing on one of the previously tested - hands. - pre_all_standing: bool - Is set to True at the top level. - top_level: bool - If the input hand is _all_ if the player's hands. If - False, it's one of the hands resulting from a split. + The hands to test and reset. *Returns* --------- @@ -286,58 +285,24 @@ class Blackjack(): the newly drawn card. """ # If the user has not hit, they are by definition standing. - if not hand["hit"]: - hand["standing"] = True + all_standing = True + pre_all_standing = True - if not hand["standing"]: - all_standing = False - - if self._calc_hand_value(hand["hand"]) >= 21 or hand["doubled"]: + for hand in user_hands: + if not hand["hit"]: hand["standing"] = True - else: - pre_all_standing = False - hand["hit"] = False + if not hand["standing"]: + all_standing = False - if top_level: - if hand["split"] >= 1: - test_hand = hand["other hand"] - test_parameters = [ - test_hand, - all_standing, - pre_all_standing, - False - ] - standing_test = (self._test_if_standing(*test_parameters)) - hand["other hand"] = standing_test[0] - all_standing = standing_test[1] - pre_all_standing = standing_test[2] - if hand["split"] >= 2: - test_hand = hand["third hand"] - test_parameters = [ - test_hand, - all_standing, - pre_all_standing, - False - ] - standing_test = (self._test_if_standing(*test_parameters)) - hand["third hand"] = standing_test[0] - all_standing = standing_test[1] - pre_all_standing = standing_test[2] - if hand["split"] >= 3: - test_hand = hand["fourth hand"] - test_parameters = [ - test_hand, - all_standing, - pre_all_standing, - False - ] - standing_test = (self._test_if_standing(*test_parameters)) - hand["fourth hand"] = standing_test[0] - all_standing = standing_test[1] - pre_all_standing = standing_test[2] + if self._calc_hand_value(hand["hand"]) >= 21 or hand["doubled"]: + hand["standing"] = True + else: + pre_all_standing = False - return hand, all_standing, pre_all_standing + hand["hit"] = False + + return user_hands, all_standing, pre_all_standing def _blackjack_finish(self, channel: str): """ @@ -355,51 +320,37 @@ class Blackjack(): """ final_winnings = "*Final Winnings:*\n" - game = self.bot.database["blackjack games"].find_one({"_id": channel}) - - dealer_value = self._calc_hand_value(game["dealer hand"]) - dealer_blackjack = game["dealer blackjack"] - dealer_busted = game["dealer busted"] + game = self.access_document(channel) for user in game["user hands"]: - calc_winnings_parameters = [ + winnings, net_winnings, reason = self._calc_winning( game["user hands"][user], - dealer_value, - True, - dealer_blackjack, - dealer_busted - ] - winnings_calc = (self._calc_winning(*calc_winnings_parameters)) - winnings, net_winnings, reason = winnings_calc + self._calc_hand_value(game["dealer hand"]), + game["dealer blackjack"], + game["dealer busted"] + ) user_name = self.bot.database_funcs.get_name(user) if winnings < 0: - if winnings == -1: - final_winnings += "{} lost 1 GwendoBuck {}\n".format( - user_name, - reason - ) - else: - money_lost = -1 * winnings - winnings_text = f"{user_name} lost {money_lost} GwendoBucks" - winnings_text += f" {reason}\n" - final_winnings += winnings_text + final_winnings += "{user_name} lost" else: - if winnings == 1: - final_winnings += f"{user_name} won 1 GwendoBuck {reason}\n" - else: - winnings_text = f"{user_name} won {winnings} GwendoBucks" - winnings_text += f" {reason}\n" - final_winnings += winnings_text + final_winnings += f"{user_name} won" + + if abs(winnings) == 1: + final_winnings += " 1 GwendoBuck" + else: + final_winnings += f" {abs(winnings)} GwendoBucks" + + winnings_text += f" {reason}\n" self.bot.money.addMoney(user, net_winnings) - self.bot.database["blackjack games"].delete_one({"_id": channel}) + self._delete_document(channel) return final_winnings - def _calc_winning(self, hand: dict, dealer_value: int, top_level: bool, + def _calc_winning(self, user_hands: dict, dealer_value: int, dealer_blackjack: bool, dealer_busted: bool): """ Calculate how much a user has won/lost in the blackjack game. @@ -431,151 +382,36 @@ class Blackjack(): """ self.bot.log("Calculating winnings") reason = "" - bet = hand["bet"] - winnings = -1 * bet + winnings = 0 net_winnings = 0 - hand_value = self._calc_hand_value(hand["hand"]) - if hand["blackjack"] and not dealer_blackjack: - reason += "(blackjack)" - winnings += math.floor(2.5 * bet) - net_winnings += math.floor(2.5 * bet) - elif dealer_blackjack: - reason += "(dealer blackjack)" - elif hand["busted"]: - reason += "(busted)" - else: - if dealer_busted: - reason = "(dealer busted)" - winnings += 2 * bet - net_winnings += 2 * bet - elif hand_value > dealer_value: - winnings += 2 * bet - net_winnings += 2 * bet - reason = "(highest value)" - elif hand_value == dealer_value: - reason = "(pushed)" - winnings += bet - net_winnings += bet + for hand in user_hands: + bet = hand["bet"] + winnings += -1 * bet + + if hand["blackjack"] and not dealer_blackjack: + reason += "\n(blackjack)" + net_winnings += math.floor(2.5 * bet) + elif dealer_blackjack: + reason += "\n(dealer blackjack)" + elif hand["busted"]: + reason += "\n(busted)" else: - reason = "(highest value)" + hand_value = self._calc_hand_value(hand["hand"]) + if dealer_busted: + reason += "\n(dealer busted)" + winnings += 2 * bet + elif hand_value > dealer_value: + winnings += 2 * bet + reason += "\n(highest value)" + elif hand_value == dealer_value: + reason += "\n(pushed)" + winnings += bet + else: + reason += "\n(highest value)" - if top_level: - if hand["split"] >= 1: - calc_winnings_parameters = [ - hand["other hand"], - dealer_value, - False, - dealer_blackjack, - dealer_busted - ] - winnings_calc = self._calc_winning(*calc_winnings_parameters) - winnings_temp, net_winnings_temp, reason_temp = winnings_calc - winnings += winnings_temp - net_winnings += net_winnings_temp - reason += reason_temp - if hand["split"] >= 2: - calc_winnings_parameters = [ - hand["third hand"], - dealer_value, - False, - dealer_blackjack, - dealer_busted - ] - winnings_calc = self._calc_winning(*calc_winnings_parameters) - winnings_temp, net_winnings_temp, reason_temp = winnings_calc - winnings += winnings_temp - net_winnings += net_winnings_temp - reason += reason_temp - if hand["split"] >= 3: - calc_winnings_parameters = [ - hand["fourth hand"], - dealer_value, - False, - dealer_blackjack, - dealer_busted - ] - winnings_calc = self._calc_winning(*calc_winnings_parameters) - winnings_temp, net_winnings_temp, reason_temp = winnings_calc - winnings += winnings_temp - net_winnings += net_winnings_temp - reason += reason_temp - - return winnings, net_winnings, reason - - def _get_hand_number(self, user: dict, hand_number: int): - """ - Get the hand with the given number. - - *Parameters* - ------------ - user: dict - The full hand dict of the user. - hand_number: int - The number of the hand to get. - - *Returns* - --------- - hand: dict - The hand. - hand_number: int - The same as hand_number, except if the user hasn't - split. If the user hasn't split, returns 0. - """ - hand = None - - if user["split"] == 0: - hand = user - hand_number = 0 - else: - if hand_number != 0: - if hand_number == 1: - hand = user - elif hand_number == 2: - hand = user["other hand"] - elif hand_number == 3: - hand = user["third hand"] - elif hand_number == 4: - hand = user["fourth hand"] - - return hand, hand_number - - def _is_round_done(self, game: dict): - """ - Find out if the round is done. - - *Parameters* - ------------ - game: dict - The game to check. - - *Returns* - --------- - round_done: bool - Whether the round is done. - """ - round_done = True - - for person in game["user hands"].values(): - if (not person["hit"]) and (not person["standing"]): - round_done = False - - if person["split"] > 0: - if not person["other hand"]["hit"]: - if not person["other hand"]["standing"]: - round_done = False - - if person["split"] > 1: - if not person["third hand"]["hit"]: - if not person["third hand"]["standing"]: - round_done = False - - if person["split"] > 2: - if not person["fourth hand"]["hit"]: - if not person["fourth hand"]["standing"]: - round_done = False - - return round_done + winnings += net_winnings + return winnings, net_winnings, reason[1:] async def _blackjack_loop(self, channel, game_round: int, game_id: str): """ @@ -590,136 +426,98 @@ class Blackjack(): game_id: str The ID of the game. """ - self.bot.log("Loop "+str(game_round), str(channel.id)) + self.bot.log(f"Loop {game_round}", str(channel.id)) - old_images_path = "gwendolyn/resources/games/old_images/" - old_image_path = old_images_path + f"blackjack{channel.id}" - with open(old_image_path, "r") as file_pointer: - old_image = await channel.fetch_message(int(file_pointer.read())) - - continue_data = (self._blackjack_continue(str(channel.id))) - new_message, all_standing, game_done = continue_data + new_message, all_standing, game_done = self._blackjack_continue( + str(channel.id) + ) if new_message != "": self.bot.log(new_message, str(channel.id)) await channel.send(new_message) if not game_done: - await old_image.delete() - tables_path = "gwendolyn/resources/games/blackjack_tables/" - file_path = f"{tables_path}blackjack_table{channel.id}.png" - old_image = await channel.send(file=discord.File(file_path)) - with open(old_image_path, "w") as file_pointer: - file_pointer.write(str(old_image.id)) + await self._delete_old_image(channel) + await self._send_image(channel) if all_standing: await asyncio.sleep(5) else: await asyncio.sleep(120) - blackjack_games = self.bot.database["blackjack games"] - game = blackjack_games.find_one({"_id": str(channel.id)}) + game = self.access_document(str(channel.id)) if game is None: - right_round = False - else: - real_round = game["round"] or -1 - real_id = game["game_id"] or -1 - right_round = game_round == real_round and game_id == real_id + self.bot.log(f"Ending loop on round {game_round}", str(channel.id)) + return - if right_round: - if not game_done: - log_message = f"Loop {game_round} starting a new blackjack loop" - self.bot.log(log_message, str(channel.id)) - await self._blackjack_loop(channel, game_round+1, game_id) - else: - new_message = self._blackjack_finish(str(channel.id)) - await channel.send(new_message) - else: - log_message = f"Ending loop on round {game_round}" + real_round = game["round"] or -1 + real_id = game["game_id"] or -1 + if game_round != real_round or game_id != real_id: + self.bot.log(f"Ending loop on round {game_round}", str(channel.id)) + return + + if not game_done: + log_message = f"Loop {game_round} starting a new blackjack loop" self.bot.log(log_message, str(channel.id)) + await self._blackjack_loop(channel, game_round+1, game_id) + else: + new_message = self._blackjack_finish(str(channel.id)) + await channel.send(new_message) - async def hit(self, ctx: discord_slash.context.SlashContext, - hand_number: int = 0): + + async def hit(self, ctx: SlashContext, hand_number: int = 0): """ Hit on a hand. *Parameters* ------------ - ctx: discord_slash.context.SlashContext + ctx: SlashContext The context of the command. - hand_number: int = 0 + hand_number: int = 1 The number of the hand to hit. """ await self.bot.defer(ctx) channel = str(ctx.channel_id) user = f"#{ctx.author.id}" - round_done = False - blackjack_games = self.bot.database["blackjack games"] - game = blackjack_games.find_one({"_id": channel}) + game = self.access_document(channel) - if user in game["user hands"]: + valid_command = self._test_valid_command(game, user, hand_number, ctx) - user_hands = game["user hands"][user] - hand, hand_number = self._get_hand_number(user_hands, hand_number) + if not valid_command: + return - if hand is None: - log_message = "They didn't specify a hand" - send_message = "You need to specify a hand" - elif game["round"] <= 0: - log_message = "They tried to hit on the 0th round" - send_message = "You can't hit before you see your cards" - elif hand["hit"]: - log_message = "They've already hit this round" - send_message = "You've already hit this round" - elif hand["standing"]: - log_message = "They're already standing" - send_message = "You can't hit when you're standing" - else: - hand["hand"].append(self._draw_card(channel)) - hand["hit"] = True + hand = game["user hands"][user][max(hand_number-1,0)] + hand["hand"].append(self._draw_card(channel)) + hand["hit"] = True - hand_value = self._calc_hand_value(hand["hand"]) + hand_value = self._calc_hand_value(hand["hand"]) - if hand_value > 21: - hand["busted"] = True + if hand_value > 21: + hand["busted"] = True - if hand_number == 2: - hand_path = f"user hands.{user}.other hand" - elif hand_number == 3: - hand_path = f"user hands.{user}.third hand" - elif hand_number == 4: - hand_path = f"user hands.{user}.fourth hand" - else: - hand_path = f"user hands.{user}" + hand_path = f"user hands.{user}.{max(hand_number-1,0)}" - game_updater = {"$set": {hand_path: hand}} - blackjack_games.update_one({"_id": channel}, game_updater) - game = blackjack_games.find_one({"_id": channel}) - round_done = self._is_round_done(game) + self._update_document(channel, {"$set": {hand_path: hand}}) + game = self.access_document(channel) - send_message = f"{ctx.author.display_name} hit" - log_message = "They succeeded" - else: - log_message = "They tried to hit without being in the game" - send_message = "You have to enter the game before you can hit" + await ctx.send(f"{ctx.author.display_name} hit") + self.bot.log("They succeeded") - await ctx.send(send_message) - self.bot.log(log_message) - - if round_done: + if _is_round_done(game): game_id = game["game_id"] self.bot.log("Hit calling self._blackjack_loop()", channel) - await self._blackjack_loop(ctx.channel, game["round"]+1, game_id) + await self._blackjack_loop( + ctx.channel, game["round"]+1, game_id + ) - async def double(self, ctx: discord_slash.context.SlashContext, - hand_number: int = 0): + async def double(self, ctx: SlashContext, hand_number: int = 0): """ Double a hand. *Parameters* ------------ - ctx: discord_slash.context.SlashContext + ctx: SlashContext The context of the command. hand_number: int = 0 The number of the hand to double. @@ -727,86 +525,61 @@ class Blackjack(): await self.bot.defer(ctx) channel = str(ctx.channel_id) user = f"#{ctx.author.id}" - round_done = False - blackjack_games = self.bot.database["blackjack games"] - game = blackjack_games.find_one({"_id": channel}) + game = self.access_document(channel) - if user in game["user hands"]: - hand_parameters = [game["user hands"][user], hand_number] - hand, hand_number = self._get_hand_number(*hand_parameters) + balance = self.bot.money.checkBalance(user) - if hand is None: - log_message = "They didn't specify a hand" - send_message = "You need to specify a hand" - elif game["round"] <= 0: - log_message = "They tried to hit on the 0th round" - send_message = "You can't hit before you see your cards" - elif hand["hit"]: - log_message = "They've already hit this round" - send_message = "You've already hit this round" - elif hand["standing"]: - log_message = "They're already standing" - send_message = "You can't hit when you're standing" - elif len(hand["hand"]) != 2: - log_message = "They tried to double after round 1" - send_message = "You can only double on the first round" - elif self.bot.money.checkBalance(user) < hand["bet"]: - log_message = "They tried to double without being in the game" - send_message = "You can't double when you're not in the game" - else: - bet = hand["bet"] - self.bot.money.addMoney(user, -1 * bet) + valid_command = self._test_valid_command(game, user, hand_number, ctx) - hand["hand"].append(self._draw_card(channel)) - hand["hit"] = True - hand["doubled"] = True - hand["bet"] += bet + if not valid_command: + return - hand_value = self._calc_hand_value(hand["hand"]) - - if hand_value > 21: - hand["busted"] = True - - if hand_number == 2: - hand_path = f"user hands.{user}.other hand" - elif hand_number == 3: - hand_path = f"user hands.{user}.third hand" - elif hand_number == 4: - hand_path = f"user hands.{user}.fourth hand" - else: - hand_path = f"user hands.{user}" - - game_updater = {"$set": {hand_path: hand}} - blackjack_games.update_one({"_id": channel}, game_updater) - - game = blackjack_games.find_one({"_id": channel}) - round_done = self._is_round_done(game) - - send_message = self.long_strings["Blackjack double"] - user_name = self.bot.database_funcs.get_name(user) - send_message = send_message.format(bet, user_name) - log_message = "They succeeded" + if len(game["user hands"][user][max(hand_number-1,0)]["hand"]) != 2: + await ctx.send("They tried to double after round 1") + self.bot.log("You can only double on the first round") + elif balance < game["user hands"][user][max(hand_number-1,0)]["bet"]: + await ctx.send("They tried to double without having enough money") + self.bot.log("You can't double when you don't have enough money") else: - log_message = "They tried to double without being in the game" - send_message = "You can't double when you're not in the game" + hand = game["user hands"][user][max(hand_number-1,0)] + self.bot.money.addMoney(user, -1 * hand["bet"]) - await ctx.send(send_message) - self.bot.log(log_message) + hand["hand"].append(self._draw_card(channel)) + hand["hit"] = True + hand["doubled"] = True + hand["bet"] += hand["bet"] - if round_done: - game_id = game["game_id"] - self.bot.log("Double calling self._blackjack_loop()", channel) - await self._blackjack_loop(ctx.channel, game["round"]+1, game_id) + if self._calc_hand_value(hand["hand"]) > 21: + hand["busted"] = True - async def stand(self, ctx: discord_slash.context.SlashContext, - hand_number: int = 0): + hand_path = f"user hands.{user}.{max(hand_number-1,0)}" + + self._update_document(channel, {"$set": {hand_path: hand}}) + + game = self.access_document(channel) + + user_name = self.bot.database_funcs.get_name(user) + send_message = self.long_strings["Blackjack double"] + + await ctx.send(send_message.format(hand["bet"], user_name)) + self.bot.log("They succeeded") + + if _is_round_done(game): + game_id = game["game_id"] + self.bot.log("Double calling self._blackjack_loop()", channel) + await self._blackjack_loop( + ctx.channel, game["round"]+1, game_id + ) + + + async def stand(self, ctx: SlashContext, hand_number: int = 0): """ Stand on a hand. *Parameters* ------------ - ctx: discord_slash.context.SlashContext + ctx: SlashContext The context of the command. hand_number: int = 0 The number of the hand to stand on. @@ -814,67 +587,39 @@ class Blackjack(): await self.bot.defer(ctx) channel = str(ctx.channel_id) user = f"#{ctx.author.id}" - round_done = False - blackjack_games = self.bot.database["blackjack games"] - game = blackjack_games.find_one({"_id": channel}) + game = self.access_document(channel) - if game is not None and user in game["user hands"]: - hand_parameters = [game["user hands"][user], hand_number] - hand, hand_number = self._get_hand_number(*hand_parameters) + valid_command = self._test_valid_command(game, user, hand_number, ctx) - if hand is None: - send_message = "You need to specify which hand" - log_message = "They didn't specify a hand" - elif game["round"] <= 0: - send_message = "You can't stand before you see your cards" - log_message = "They tried to stand on round 0" - elif hand["hit"]: - send_message = "You've already hit this round" - log_message = "They'd already hit this round" - elif hand["standing"]: - send_message = "You're already standing" - log_message = "They're already standing" - else: - hand["standing"] = True + if not valid_command: + return - if hand_number == 2: - hand_path = f"user hands.{user}.other hand" - elif hand_number == 3: - hand_path = f"user hands.{user}.third hand" - elif hand_number == 4: - hand_path = f"user hands.{user}.fourth hand" - else: - hand_path = f"user hands.{user}" + hand = game["user hands"][user][max(hand_number-1,0)] + hand["standing"] = True - game_updater = {"$set": {hand_path: hand}} - blackjack_games.update_one({"_id": channel}, game_updater) - game = blackjack_games.find_one({"_id": channel}) - round_done = self._is_round_done(game) + hand_path = f"user hands.{user}.{max(hand_number-1,0)}" - send_message = f"{ctx.author.display_name} is standing" - log_message = "They succeeded" - - else: - log_message = "They tried to stand without being in the game" - send_message = "You have to enter the game before you can stand" + self._update_document(channel, {"$set": {hand_path: hand}}) + game = self.access_document(channel) + send_message = f"{ctx.author.display_name} is standing" + log_message = "They succeeded" await ctx.send(send_message) self.bot.log(log_message) - - if round_done: + if _is_round_done(game): game_id = game["game_id"] self.bot.log("Stand calling self._blackjack_loop()", channel) await self._blackjack_loop(ctx.channel, game["round"]+1, game_id) - async def split(self, ctx: discord_slash.context.SlashContext, - hand_number: int = 0): + + async def split(self, ctx: SlashContext, hand_number: int = 0): """ Split a hand. *Parameters* ------------ - ctx: discord_slash.context.SlashContext + ctx: SlashContext The context of the command. hand_number: int = 0 The number of the hand to split. @@ -882,141 +627,75 @@ class Blackjack(): await self.bot.defer(ctx) channel = str(ctx.channel_id) user = f"#{ctx.author.id}" - round_done = False - hand_number_error = False - blackjack_games = self.bot.database["blackjack games"] - game = blackjack_games.find_one({"_id": channel}) + game = self.access_document(channel) - if game["user hands"][user]["split"] == 0: - hand = game["user hands"][user] - new_hand = game["user hands"][user]["other hand"] - hand_number = 0 - other_hand = 2 + + valid_command = self._test_valid_command(game, user, hand_number, ctx) + + if not valid_command: + return + + old_hand = game["user hands"][user][max(hand_number-1,0)] + + if len(game["user hands"][user]) > 3: + await ctx.send("You can only split 3 times") + self.bot.log("They tried to split more than three times") + elif len(old_hand["hand"]) != 2: + await ctx.send("You can only split on the first round") + self.bot.log("They tried to split after the first round") + elif len({self._calc_hand_value([i]) for i in old_hand["hand"]}) == 1: + await ctx.send(self.long_strings["Blackjack different cards"]) + self.bot.log("They tried to split two different cards") + elif self.bot.money.checkBalance(user) < old_hand["bet"]: + await ctx.send("You don't have enough GwendoBucks") + self.bot.log("They didn't have enough GwendoBucks") else: - if hand_number == 1: - hand = game["user hands"][user] - elif hand_number == 2: - hand = game["user hands"][user]["other hand"] - elif hand_number == 3: - hand = game["user hands"][user]["third hand"] - else: - hand_number_error = True + self.bot.money.addMoney(user, -1 * old_hand["bet"]) - if game["user hands"][user]["split"] == 1: - new_hand = game["user hands"][user]["third hand"] - other_hand = 3 - else: - new_hand = game["user hands"][user]["fourth hand"] - other_hand = 4 + old_hand["hit"] = True - if hand_number_error: - log_message = "They didn't specify a hand" - send_message = "You have to specify the hand you're hitting with" - elif game["round"] == 0: - log_message = "They tried to split on round 0" - send_message = "You can't split before you see your cards" - elif game["user hands"][user]["split"] > 3: - log_message = "They tried to split more than three times" - send_message = "You can only split 3 times" - elif hand["hit"]: - log_message = "They've already hit" - send_message = "You've already hit or split this hand." - elif hand["standing"]: - log_message = "They're already standing" - send_message = "You're already standing" - elif len(hand["hand"]) != 2: - log_message = "They tried to split after the first round" - send_message = "You can only split on the first round" - else: - first_card = self._calc_hand_value([hand["hand"][0]]) - second_card = self._calc_hand_value([hand["hand"][1]]) - if first_card != second_card: - log_message = "They tried to split two different cards" - send_message = self.long_strings["Blackjack different cards"] - else: - bet = hand["bet"] - if self.bot.money.checkBalance(user) < bet: - log_message = "They didn't have enough GwendoBucks" - send_message = "You don't have enough GwendoBucks" - else: - self.bot.money.addMoney(user, -1 * bet) + hands = [old_hand, { + "hand": [old_hand["hand"].pop(1), self._draw_card(channel)], + "bet": old_hand["bet"], "standing": False, + "busted": False, "blackjack": False, "hit": True, + "doubled": False + }] + old_hand["hand"].append(self._draw_card(channel)) - hand["hit"] = True - new_hand["hit"] = True + for i, hand in enumerate(hands): + if self._calc_hand_value(hand["hand"]) > 21: + hands[i]["busted"] = True + elif self._calc_hand_value(hand["hand"]) == 21: + hands[i]["blackjack"] = True - new_hand = { - "hand": [], "bet": 0, "standing": False, - "busted": False, "blackjack": False, "hit": True, - "doubled": False - } + game["user hands"][user][max(hand_number-1,0)] = hands[0] + game["user hands"][user].append(hands[1]) - new_hand["bet"] = hand["bet"] + updater = {"$set": {f"user hands.{user}": game["user hands"][user]}} + self._update_document(channel, updater) - new_hand["hand"].append(hand["hand"].pop(1)) - new_hand["hand"].append(self._draw_card(channel)) - hand["hand"].append(self._draw_card(channel)) - hand["hit"] = True + send_message = self.long_strings["Blackjack split"] + user_name = self.bot.database_funcs.get_name(user) - hand_value = self._calc_hand_value(hand["hand"]) - other_hand_value = self._calc_hand_value(new_hand["hand"]) - if hand_value > 21: - hand["busted"] = True - elif hand_value == 21: - hand["blackjack"] = True + await ctx.send(send_message.format(user_name)) + self.bot.log("They succeeded") - if other_hand_value > 21: - new_hand["busted"] = True - elif other_hand_value == 21: - new_hand["blackjack"] = True + game = self.access_document(channel) + if _is_round_done(game): + game_id = game["game_id"] + self.bot.log("Split calling self._blackjack_loop()", channel) + await self._blackjack_loop( + ctx.channel, game["round"]+1, game_id + ) - if hand_number == 2: - hand_path = f"user hands.{user}.other hand" - elif hand_number == 3: - hand_path = f"user hands.{user}.third hand" - else: - hand_path = f"user hands.{user}" - - game_updater = {"$set": {hand_path: hand}} - blackjack_games.update_one({"_id": channel}, game_updater) - - if other_hand == 3: - other_hand_path = f"user hands.{user}.third hand" - elif other_hand == 4: - other_hand_path = f"user hands.{user}.fourth hand" - else: - other_hand_path = f"user hands.{user}.other hand" - - game_updater = {"$set": {other_hand_path: new_hand}} - blackjack_games.update_one({"_id": channel}, game_updater) - - split_updater = {"$inc": {"user hands."+user+".split": 1}} - blackjack_games.update_one({"_id": channel}, split_updater) - - game = blackjack_games.find_one({"_id": channel}) - round_done = self._is_round_done(game) - - send_message = self.long_strings["Blackjack split"] - user_name = self.bot.database_funcs.get_name(user) - send_message = send_message.format(user_name) - log_message = "They succeeded" - - await ctx.send(send_message) - self.bot.log(log_message) - - if round_done: - game_id = game["game_id"] - self.bot.log("Stand calling self._blackjack_loop()", channel) - await self._blackjack_loop(ctx.channel, game["round"]+1, game_id) - - async def enter_game(self, ctx: discord_slash.context.SlashContext, - bet: int): + async def enter_game(self, ctx: SlashContext, bet: int): """ Enter the blackjack game. *Parameters* ------------ - ctx: discord_slash.context.SlashContext + ctx: SlashContext The context of the command. bet: int The bet to enter with. @@ -1024,8 +703,8 @@ class Blackjack(): 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 = self.access_document(channel) user_name = self.bot.database_funcs.get_name(user) self.bot.log(f"{user_name} is trying to join the Blackjack game") @@ -1052,35 +731,30 @@ class Blackjack(): self.bot.money.addMoney(user, -1 * bet) player_hand = [self._draw_card(channel) for _ in range(2)] - hand_value = self._calc_hand_value(player_hand) - - blackjack_hand = (hand_value == 21) - - new_hand = { + new_hand = [{ "hand": player_hand, "bet": bet, "standing": False, - "busted": False, "blackjack": blackjack_hand, - "hit": True, "doubled": False, "split": 0, - "other hand": {}, "third hand": {}, "fourth hand": {} - } + "busted": False, + "blackjack": self._calc_hand_value(player_hand) == 21, + "hit": True, "doubled": False + }] - function = {"$set": {f"user hands.{user}": new_hand}} - collection.update_one({"_id": channel}, function) + updater = {"$set": {f"user hands.{user}": new_hand}} + self._update_document(channel, updater) - enter_game_text = "entered the game with a bet of" - bet_text = f"{bet} GwendoBucks" - send_message = f"{user_name} {enter_game_text} {bet_text}" + send_message = "{} entered the game with a bet of {} GwendoBucks" + send_message = send_message.format(user_name, bet) log_message = send_message self.bot.log(log_message) await ctx.send(send_message) - async def start(self, ctx: discord_slash.context.SlashContext): + async def start(self, ctx: SlashContext): """ Start a blackjack game. *Parameters* ------------ - ctx: discord_slash.context.SlashContext + ctx: SlashContext The context of the command. """ await self.bot.defer(ctx) @@ -1090,165 +764,166 @@ class Blackjack(): self.bot.log("Starting blackjack game") await ctx.send("Starting a new game of blackjack") cards_left = 0 - cards = self.bot.database["blackjack cards"].find_one({"_id": channel}) + cards = self.access_document(channel, "cards") if cards is not None: cards_left = len(cards["cards"]) # Shuffles if not enough cards if cards_left < blackjack_min_cards: - self._blackjack_shuffle(channel) + self._shuffle_cards(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.access_document(channel) - self.bot.log("Trying to start a blackjack game in "+channel) - game_started = False + self.bot.log(f"Trying to start a blackjack game in {channel}") - if game is None: - - dealer_hand = [self._draw_card(channel), self._draw_card(channel)] - game_id = datetime.datetime.now().strftime('%Y%m%d%H%M%S') - - new_game = { - "_id": channel, "dealer hand": dealer_hand, - "dealer busted": False, "dealer blackjack": False, - "user hands": {}, "all standing": False, "round": 0, - "game_id": game_id - } - - if self._calc_hand_value(dealer_hand) == 21: - new_game["dealer blackjack"] = True - - self.bot.database["blackjack games"].insert_one(new_game) - - table_images_path = "gwendolyn/resources/games/blackjack_tables/" - empty_table_image = "gwendolyn/resources/games/blackjack_table.png" - new_table_image = f"{table_images_path}blackjack_table{channel}.png" - copyfile(empty_table_image, new_table_image) - - game_started = True - - if game_started: - send_message = self.long_strings["Blackjack started"] - await ctx.channel.send(send_message) - - table_images_path = "gwendolyn/resources/games/blackjack_tables/" - file_path = f"{table_images_path}blackjack_table{channel}.png" - - old_image = await ctx.channel.send(file=discord.File(file_path)) - old_images_path = "gwendolyn/resources/games/old_images/blackjack" - - with open(old_images_path+channel, "w") as file_pointer: - file_pointer.write(str(old_image.id)) - - await asyncio.sleep(30) - - game_done = False - - blackjack_games = self.bot.database["blackjack games"] - game = blackjack_games.find_one({"_id": channel}) - - if len(game["user hands"]) == 0: - game_done = True - send_message = "No one entered the game. Ending the game." - await ctx.channel.send(send_message) - - game_id = game["game_id"] - - # Loop of game rounds - if not game_done: - self.bot.log("start() calling _blackjack_loop()", channel) - await self._blackjack_loop(ctx.channel, 1, game_id) - else: - new_message = self._blackjack_finish(channel) - await ctx.channel.send(new_message) - else: - send_message = self.long_strings["Blackjack going on"] - await ctx.channel.send(send_message) + if game is not None: + await ctx.channel.send(self.long_strings["Blackjack going on"]) self.bot.log("There was already a game going on") + return - async def hilo(self, ctx: discord_slash.context.SlashContext): + dealer_hand = [self._draw_card(channel), self._draw_card(channel)] + game_id = datetime.datetime.now().strftime('%Y%m%d%H%M%S') + + new_game = { + "_id": channel, "dealer hand": dealer_hand, + "dealer busted": False, "game_id": game_id, + "dealer blackjack": (self._calc_hand_value(dealer_hand) == 21), + "user hands": {}, "all standing": False, "round": 0 + } + + await ctx.channel.send(self.long_strings["Blackjack started"]) + await self._start_new(ctx.channel, new_game) + + await asyncio.sleep(30) + game = self.access_document(channel) + + if len(game["user hands"]) == 0: + await ctx.channel.send("No one entered the game. Ending the game.") + await ctx.channel.send(self._blackjack_finish(channel)) + return + + game_id = game["game_id"] + + # Loop of game rounds + self.bot.log("start() calling _blackjack_loop()", channel) + await self._blackjack_loop(ctx.channel, 1, game_id) + + async def hilo(self, ctx: SlashContext): """ Get the hilo of the blackjack game. *Parameters* ------------ - ctx: discord_slash.context.SlashContext + ctx: 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" + data = self.access_document(str(ctx.channel_id), "hilo") + hilo = data["hilo"] if data else 0 + await ctx.send(f"Hi-lo value: {hilo}", hidden=True) - async def shuffle(self, ctx: discord_slash.context.SlashContext): + async def shuffle(self, ctx: SlashContext): """ Shuffle the cards used for blackjack. *Parameters* ------------ - ctx: discord_slash.context.SlashContext + ctx: SlashContext The context of the command. """ - channel = ctx.channel_id - self._blackjack_shuffle(str(channel)) - self.bot.log("Shuffling the blackjack deck...", str(channel)) + self._shuffle_cards(str(ctx.channel_id)) + self.bot.log("Shuffling the blackjack deck...", str(ctx.channel_id)) await ctx.send("Shuffling the deck...") - async def cards(self, ctx: discord_slash.context.SlashContext): + async def cards(self, ctx: SlashContext): """ Get how many cards are left for blackjack. *Parameters* ------------ - ctx: discord_slash.context.SlashContext + ctx: SlashContext The context of the command. """ - channel = ctx.channel_id - cards_left = 0 - blackjack_games = self.bot.database["blackjack cards"] - cards = blackjack_games.find_one({"_id": str(channel)}) - if cards is not None: - cards_left = len(cards["cards"]) - + cards = self.access_document(str(ctx.channel_id), "cards") + cards_left = len(cards["cards"]) if cards else 0 decks_left = round(cards_left/52, 1) + send_message = f"Cards left:\n{cards_left} cards, {decks_left} decks" await ctx.send(send_message, hidden=True) -class DrawBlackjack(): +class DrawBlackjack(CardDrawer): """ Draws the blackjack image. *Methods* --------- - draw_image(channel: str) *Attributes* ------------ bot: Gwendolyn The instance of the bot. - BORDER: int - The size of the border in pixels. PLACEMENT: list The order to place the user hands in. """ # pylint: disable=too-few-public-methods - def __init__(self, bot): + def __init__(self, bot, game: Blackjack): """Initialize the class.""" - self.bot = bot + super().__init__(bot, game) + # pylint: disable=invalid-name - self.BORDER = 100 - self.SMALLBORDER = int(self.BORDER/3.5) self.PLACEMENT = [2, 1, 3, 0, 4] # pylint: enable=invalid-name + self._set_card_size(197) - def draw_image(self, channel: str): + def _draw_dealer_hand(self, game: dict, table: Image.Image): + dealer_hand = self._draw_hand( + game["dealer hand"], + game["all standing"], + game["dealer busted"] if game["all standing"] else False, + game["dealer blackjack"] if game["all standing"] else False + ) + + paste_position = (800-self.CARD_BORDER, 20-self.CARD_BORDER) + table.paste(dealer_hand, paste_position, dealer_hand) + + def _draw_player_name(self, user: str, placement: int, table: Image.Image): + user_name = self.bot.database_funcs.get_name(user) + font, font_size = self._adjust_font(50, user_name, 360) + text_width, text_height = font.getsize(user_name) + + text_x = 32+(384*placement)+117-(text_width//2) + text_y = 960+text_height + + colors = [BLACK, WHITE] + text_image = self._draw_shadow_text(user_name, colors, font_size) + table.paste(text_image, (text_x, text_y), text_image) + + def _draw_player_hands(self, all_hands: dict, table: Image.Image): + for i, (user, user_hands) in enumerate(all_hands.items()): + hand_images = [] + position_x = 32-self.CARD_BORDER+(384*self.PLACEMENT[i]) + + for hand in user_hands: + hand_images.append(self._draw_hand( + hand["hand"], + False, + hand["busted"], + hand["blackjack"] + )) + + for index, image in enumerate(hand_images): + position_y = 840 + position_y -= self.CARD_BORDER + (140 * (len(user_hands)-index)) + position = (position_x, position_y) + table.paste(image, position, image) + + self._draw_player_name(user, self.PLACEMENT[i], table) + + def _draw_image(self, game: dict, table: Image.Image): """ Draw the table image. @@ -1257,140 +932,8 @@ class DrawBlackjack(): channel: str The id of the channel the game is in. """ - self.bot.log("Drawing blackjack table", channel) - game = self.bot.database["blackjack games"].find_one({"_id": channel}) - - fonts_path = "gwendolyn/resources/fonts/" - font = ImageFont.truetype(fonts_path+'futura-bold.ttf', 50) - small_font = ImageFont.truetype(fonts_path+'futura-bold.ttf', 40) - - table = Image.open("gwendolyn/resources/games/blackjack_table.png") - text_image = ImageDraw.Draw(table) - hands = game["user hands"] - - dealer_busted = game["dealer busted"] - dealer_blackjack = game["dealer blackjack"] - - if not game["all standing"]: - hand_parameters = [ - game["dealer hand"], - True, - False, - False - ] - else: - hand_parameters = [ - game["dealer hand"], - False, - dealer_busted, - dealer_blackjack - ] - - dealer_hand = self._draw_hand(*hand_parameters) - - paste_position = (800-self.SMALLBORDER, 20-self.SMALLBORDER) - table.paste(dealer_hand, paste_position, dealer_hand) - - for i in range(len(hands)): - key, value = list(hands.items())[i] - key = self.bot.database_funcs.get_name(key) - hand_parameters = [ - value["hand"], - False, - value["busted"], - value["blackjack"] - ] - user_hand = self._draw_hand(*hand_parameters) - position_x = 32-self.SMALLBORDER+(384*self.PLACEMENT[i]) - - if value["split"] >= 1: - hand_parameters_two = [ - value["other hand"]["hand"], - False, - value["other hand"]["busted"], - value["other hand"]["blackjack"] - ] - user_other_hand = self._draw_hand(*hand_parameters_two) - - if value["split"] >= 2: - hand_parameters_three = [ - value["third hand"]["hand"], - False, - value["third hand"]["busted"], - value["third hand"]["blackjack"] - ] - user_third_hand = self._draw_hand(*hand_parameters_three) - - if value["split"] >= 3: - hand_parameters_four = [ - value["fourth hand"]["hand"], - False, - value["fourth hand"]["busted"], - value["fourth hand"]["blackjack"] - ] - user_fourth_hand = self._draw_hand(*hand_parameters_four) - - if value["split"] == 3: - position_one = (position_x, 280-self.SMALLBORDER) - position_two = (position_x, 420-self.SMALLBORDER) - position_three = (position_x, 560-self.SMALLBORDER) - position_four = (position_x, 700-self.SMALLBORDER) - - table.paste(user_hand, position_one, user_hand) - table.paste(user_other_hand, position_two, user_other_hand) - table.paste(user_third_hand, position_three, user_third_hand) - table.paste(user_fourth_hand, position_four, user_fourth_hand) - elif value["split"] == 2: - position_one = (position_x, 420-self.SMALLBORDER) - position_two = (position_x, 560-self.SMALLBORDER) - position_three = (position_x, 700-self.SMALLBORDER) - - table.paste(user_hand, position_one, user_hand) - table.paste(user_other_hand, position_two, user_other_hand) - table.paste(user_third_hand, position_three, user_third_hand) - elif value["split"] == 1: - position_one = (position_x, 560-self.SMALLBORDER) - position_two = (position_x, 700-self.SMALLBORDER) - - table.paste(user_hand, position_one, user_hand) - table.paste(user_other_hand, position_two, user_other_hand) - else: - position_one = (position_x, 680-self.SMALLBORDER) - table.paste(user_hand, position_one, user_hand) - - text_width = font.getsize(key)[0] - text_x = 32+(384*self.PLACEMENT[i])+117-int(text_width/2) - black = (0, 0, 0) - white = (255, 255, 255) - - if text_width < 360: - # Black shadow behind and slightly below white text - text_image.text((text_x-3, 1010-3), key, fill=black, font=font) - text_image.text((text_x+3, 1010-3), key, fill=black, font=font) - text_image.text((text_x-3, 1010+3), key, fill=black, font=font) - text_image.text((text_x+3, 1010+3), key, fill=black, font=font) - text_image.text((text_x, 1005), key, fill=white, font=font) - else: - text_width = small_font.getsize(key)[0] - positions = [ - (text_x-2, 1020-2), - (text_x+2, 1020-2), - (text_x-2, 1020+2), - (text_x+2, 1020+2), - (text_x, 1015) - ] - text_image.text(positions[0], key, fill=black, font=small_font) - text_image.text(positions[1], key, fill=black, font=small_font) - text_image.text(positions[2], key, fill=black, font=small_font) - text_image.text(positions[3], key, fill=black, font=small_font) - text_image.text(positions[4], key, fill=white, font=small_font) - - self.bot.log("Saving table image") - table_images_path = "gwendolyn/resources/games/blackjack_tables/" - table_image_path = f"{table_images_path}blackjack_table{channel}.png" - table.save(table_image_path) - - return + self._draw_dealer_hand(game, table) + self._draw_player_hands(game["user hands"], table) def _draw_hand(self, hand: dict, dealer: bool, busted: bool, blackjack: bool): @@ -1415,85 +958,24 @@ class DrawBlackjack(): The image of the hand. """ self.bot.log("Drawing hand {hand}, {busted}, {blackjack}") - fonts_path = "gwendolyn/resources/fonts/" - font = ImageFont.truetype(fonts_path+'futura-bold.ttf', 200) - font_two = ImageFont.truetype(fonts_path+'futura-bold.ttf', 120) - length = len(hand) - background_width = (self.BORDER*2)+691+(125*(length-1)) - background_size = (background_width, (self.BORDER*2)+1065) - background = Image.new("RGBA", background_size, (0, 0, 0, 0)) - text_image = ImageDraw.Draw(background) - card_y = self.BORDER+self.PLACEMENT[1] - cards_path = "gwendolyn/resources/games/cards/" + background = self._draw_cards(hand, int(dealer)) - if dealer: - img = Image.open(cards_path+hand[0].upper()+".png") - card_position = (self.BORDER+self.PLACEMENT[0], card_y) - background.paste(img, card_position, img) - img = Image.open("gwendolyn/resources/games/cards/red_back.png") - card_position = (125+self.BORDER+self.PLACEMENT[0], card_y) - background.paste(img, card_position, img) - else: - for i in range(length): - card_path = cards_path + f"{hand[i].upper()}.png" - img = Image.open(card_path) - card_position = (self.BORDER+(i*125)+self.PLACEMENT[0], card_y) - background.paste(img, card_position, img) + if busted or blackjack: + if busted: + text = "BUSTED" + last_color = RED + font_size = 60 + elif blackjack: + text = "BLACKJACK" + last_color = GOLD + font_size = 35 - width, height = background.size - text_height = 290+self.BORDER - white = (255, 255, 255) - black = (0, 0, 0) - red = (255, 50, 50) - gold = (155, 123, 0) + colors = [BLACK, WHITE, last_color] + text_image = self._draw_shadow_text(text, colors, font_size) + background.paste(text_image, text_position, text_image) - if busted: - text_width = font.getsize("BUSTED")[0] - text_x = int(width/2)-int(text_width/2) - positions = [ - (text_x-10, text_height+20-10), - (text_x+10, text_height+20-10), - (text_x-10, text_height+20+10), - (text_x+10, text_height+20+10), - (text_x-5, text_height-5), - (text_x+5, text_height-5), - (text_x-5, text_height+5), - (text_x+5, text_height+5), - (text_x, text_height) - ] - text_image.text(positions[0], "BUSTED", fill=black, font=font) - text_image.text(positions[1], "BUSTED", fill=black, font=font) - text_image.text(positions[2], "BUSTED", fill=black, font=font) - text_image.text(positions[3], "BUSTED", fill=black, font=font) - text_image.text(positions[4], "BUSTED", fill=white, font=font) - text_image.text(positions[5], "BUSTED", fill=white, font=font) - text_image.text(positions[6], "BUSTED", fill=white, font=font) - text_image.text(positions[7], "BUSTED", fill=white, font=font) - text_image.text(positions[8], "BUSTED", fill=red, font=font) - elif blackjack: - text_width = font_two.getsize("BLACKJACK")[0] - text_x = int(width/2)-int(text_width/2) - positions = [ - (text_x-6, text_height+20-6), - (text_x+6, text_height+20-6), - (text_x-6, text_height+20+6), - (text_x+6, text_height+20+6), - (text_x-3, text_height-3), - (text_x+3, text_height-3), - (text_x-3, text_height+3), - (text_x+3, text_height+3), - (text_x, text_height) - ] - text = "BLACKJACK" - text_image.text(positions[0], text, fill=black, font=font_two) - text_image.text(positions[1], text, fill=black, font=font_two) - text_image.text(positions[2], text, fill=black, font=font_two) - text_image.text(positions[3], text, fill=black, font=font_two) - text_image.text(positions[4], text, fill=white, font=font_two) - text_image.text(positions[5], text, fill=white, font=font_two) - text_image.text(positions[6], text, fill=white, font=font_two) - text_image.text(positions[7], text, fill=white, font=font_two) - text_image.text(positions[8], text, fill=gold, font=font_two) + width = background.size[0] + text_width = self._get_font(font_size).getsize(text)[0] + text_position = (int(width/2)-int(text_width/2), 85) - resized_size = (int(width/3.5), int(height/3.5)) - return background.resize(resized_size, resample=Image.BILINEAR) + return background diff --git a/gwendolyn/funcs/games/game_base.py b/gwendolyn/funcs/games/game_base.py new file mode 100644 index 0000000..3a2a1c4 --- /dev/null +++ b/gwendolyn/funcs/games/game_base.py @@ -0,0 +1,219 @@ +"""Base class for the games.""" +import random +from shutil import copyfile + +from discord import File +from discord.abc import Messageable + +from PIL import ImageFont, Image, ImageDraw + +class GameBase(): + """The base class for the games.""" + + def __init__(self, bot): + """Initialize the class.""" + self.bot = bot + self.database = self.bot.database + self.long_strings = self.bot.long_strings + self.resources = "gwendolyn/resources/games/" + self.game_name = "" + self.old_images_path = f"{self.resources}old_images/{self.game_name}" + self.draw = BaseDrawer(bot, self) + + def access_document(self, channel: str, collection_name: str="games"): + collection = self.bot.database[f"{self.game_name} {collection_name}"] + return collection.find_one({"_id": channel}) + + def _update_document(self, channel: str, updater: dict, + collection_name: str="games"): + self.database[f"{self.game_name} {collection_name}"].update_one( + {"_id": channel}, + updater, + upsert=True + ) + + def _delete_document(self, channel: str, collection_name: str="games"): + self.database[f"{self.game_name} {collection_name}"].delete_one( + {"_id": channel} + ) + + def _insert_document(self, data: dict, collection_name: str="games"): + self.database[f"{self.game_name} {collection_name}"].insert_one(data) + + async def _delete_old_image(self, channel: Messageable): + with open(self.old_images_path + str(channel.id), "r") as file_pointer: + old_image = await channel.fetch_message(int(file_pointer.read())) + + await old_image.delete() + + async def _send_image(self, channel: Messageable): + self.draw.draw(str(channel.id)) + file_path = f"{self.resources}images/blackjack{channel.id}.png" + old_image = await channel.send(file=File(file_path)) + with open(self.old_images_path + str(channel.id), "w") as file_pointer: + file_pointer.write(str(old_image.id)) + + async def _start_new(self, channel: Messageable, new_game: dict): + self._insert_document(new_game) + default_image = f"{self.resources}default_images/{self.game_name}.png" + new_image = f"{self.resources}images/{self.game_name}{channel.id}.png" + copyfile(default_image, new_image) + + await self._send_image(channel) + + +class CardGame(GameBase): + """The class for card games.""" + def __init__(self, bot): + """Initialize the class.""" + super().__init__(bot) + self.decks_used = 1 + + def _shuffle_cards(self, channel: str): + self.bot.log(f"Shuffling cards for {self.game_name}") + with open(f"{self.resources}deck_of_cards.txt", "r") as file_pointer: + deck = file_pointer.read() + + all_decks = deck.split("\n") * self.decks_used + random.shuffle(all_decks) + + self._update_document( + channel, + {"$set": {"_id": channel, "cards": all_decks}}, + "cards" + ) + + def _draw_card(self, channel: str): + """ + Draw a card from the stack. + + *Parameters* + ------------ + channel: str + The id of the channel the card is drawn in. + """ + self.bot.log("drawing a card") + + drawn_card = self.access_document(channel, "cards")["cards"][0] + self._update_document(channel, {"$pop": {"cards": -1}}, "cards") + + return drawn_card + +class BaseDrawer(): + """Class for drawing games.""" + def __init__(self, bot, game: GameBase): + self.bot = bot + self.game = game + self.fonts_path = "gwendolyn/resources/fonts/" + self.font_name = "futura-bold" + + self.resources = game.resources + game_name = game.game_name + self.default_image = f"{self.resources}default_images/{game_name}.png" + self.images_path = f"{self.resources}images/{game_name}" + + def _draw_image(self, game: dict, table: Image.Image): + pass + + def draw(self, channel: str): + game = self.game.access_document(channel) + image = Image.open(self.default_image) + self._draw_image(game, image) + self._save_image(image, channel) + + def _get_font(self, size: int, font_name: str = None): + if font_name is None: + font_name = self.font_name + return ImageFont.truetype(f"{self.fonts_path}{font_name}.ttf", size) + + def _adjust_font(self, max_font_size: int, text: str, max_text_size: int, + font_name: str = None): + font_size = max_font_size + font = self._get_font(font_size, font_name) + text_width = font.getsize(text)[0] + + while text_width > max_text_size: + font_size -= 1 + font = self._get_font(font_size, font_name) + text_width = font.getsize(text)[0] + + return font, font_size + + def _save_image(self, image: Image.Image, channel: str): + self.bot.log("Saving image") + image.save(f"{self.images_path}{channel}.png") + + def _draw_shadow_text(self, text: str, colors: list[tuple[int, int, int]], + font_size: int): + font = self._get_font(font_size) + offset = font_size//20 + shadow_offset = font_size//10 + + text_size = list(font.getsize(text)) + text_size[0] += 1 + (offset * 2) + text_size[1] += 1 + (offset * 2) + shadow_offset + + image = Image.new("RGBA", tuple(text_size), (0, 0, 0, 0)) + text_image = ImageDraw.Draw(image) + + for color_index, color in enumerate(colors[:-1]): + color_offset = offset//(color_index+1) + if color_index == 0: + color_shadow_offset = shadow_offset + else: + color_shadow_offset = 0 + + for i in range(4): + x_pos = [-1,1][i % 2] * color_offset + y_pos = [-1,1][(i//2) % 2] * color_offset + color_shadow_offset + position = (offset + x_pos, offset + y_pos) + text_image.text(position, text, fill=color, font=font) + + text_image.text((offset, offset), text, fill=colors[-1], font=font) + + return image + +class CardDrawer(BaseDrawer): + def __init__(self, bot, game: GameBase): + super().__init__(bot, game) + self.cards_path = f"{self.resources}cards/" + + # pylint: disable=invalid-name + self.CARD_WIDTH = 691 + self.CARD_HEIGHT = 1065 + + self.CARD_BORDER = 100 + self.CARD_OFFSET = 125 + # pylint: enable=invalid-name + + def _set_card_size(self, wanted_card_width: int): + ratio = wanted_card_width / self.CARD_WIDTH + self.CARD_WIDTH = wanted_card_width + self.CARD_HEIGHT = int(self.CARD_HEIGHT * ratio) + self.CARD_BORDER = int(self.CARD_BORDER * ratio) + self.CARD_OFFSET = int(self.CARD_OFFSET * ratio) + + def _draw_cards(self, hand: list[str], hidden_cards: int = 0): + image_width = (self.CARD_BORDER * 2) + self.CARD_WIDTH + image_width += (self.CARD_OFFSET * (len(hand)-1)) + image_size = (image_width, (self.CARD_BORDER * 2) + self.CARD_HEIGHT) + background = Image.new("RGBA", image_size, (0, 0, 0, 0)) + + for i, card in enumerate(hand): + position = ( + self.CARD_BORDER + (self.CARD_OFFSET*i), + self.CARD_BORDER + ) + + if i + hidden_cards < len(hand): + card_image = Image.open(f"{self.cards_path}{card.upper()}.png") + else: + card_image = Image.open(f"{self.cards_path}red_back.png") + + card_image = card_image.resize( + (self.CARD_WIDTH, self.CARD_HEIGHT), + resample=Image.BILINEAR + ) + background.paste(card_image, position, card_image) + + return background diff --git a/gwendolyn/gwendolyn_client.py b/gwendolyn/gwendolyn_client.py index 850827a..d77fd82 100644 --- a/gwendolyn/gwendolyn_client.py +++ b/gwendolyn/gwendolyn_client.py @@ -94,7 +94,7 @@ class Gwendolyn(commands.Bot): if filename.endswith(".py"): self.load_extension(f"gwendolyn.cogs.{filename[:-3]}") - def log(self, messages, channel: str = "", level: int = 20): + def log(self, messages, channel: str = "", level: int = 20): # pylint:disable=no-self-use """Log a message. Described in utils/util_functions.py.""" log_this(messages, channel, level) diff --git a/gwendolyn/resources/games/blackjack_table.png b/gwendolyn/resources/games/default_images/blackjack.png similarity index 100% rename from gwendolyn/resources/games/blackjack_table.png rename to gwendolyn/resources/games/default_images/blackjack.png diff --git a/gwendolyn/resources/paper.jpg b/gwendolyn/resources/games/default_images/hangman.jpg similarity index 100% rename from gwendolyn/resources/paper.jpg rename to gwendolyn/resources/games/default_images/hangman.jpg diff --git a/gwendolyn/utils/util_functions.py b/gwendolyn/utils/util_functions.py index c2a6cf8..b2799d6 100644 --- a/gwendolyn/utils/util_functions.py +++ b/gwendolyn/utils/util_functions.py @@ -16,12 +16,12 @@ Contains utility functions used by parts of the bot. new_string: str) -> str emoji_to_command(emoji: str) -> str """ +import string import json # Used by long_strings(), get_params() and make_files() import logging # Used for logging import os # Used by make_files() to check if files exist import sys # Used to specify printing for logging import imdb # Used to disable logging for the module -import string BASE_37 = ":" + string.digits + string.ascii_uppercase