diff --git a/gwendolyn/cogs/game_cog.py b/gwendolyn/cogs/game_cog.py index 36b0e97..c038560 100644 --- a/gwendolyn/cogs/game_cog.py +++ b/gwendolyn/cogs/game_cog.py @@ -52,26 +52,6 @@ class BlackjackCog(commands.Cog): """Enter the game of blackjack with a bet.""" await self.bot.games.blackjack.enter_game(ctx, bet) - @cog_ext.cog_subcommand(**params["blackjack_stand"]) - async def blackjack_stand(self, ctx, hand=0): - """Stand on your hand in blackjack.""" - await self.bot.games.blackjack.stand(ctx, hand) - - @cog_ext.cog_subcommand(**params["blackjack_hit"]) - async def blackjack_hit(self, ctx, hand=0): - """Hit on your hand in blackjack.""" - await self.bot.games.blackjack.hit(ctx, hand) - - @cog_ext.cog_subcommand(**params["blackjack_double"]) - async def blackjack_double(self, ctx, hand=0): - """Double in blackjack.""" - await self.bot.games.blackjack.double(ctx, hand) - - @cog_ext.cog_subcommand(**params["blackjack_split"]) - async def blackjack_split(self, ctx, hand=0): - """Split your hand in blackjack.""" - await self.bot.games.blackjack.split(ctx, hand) - @cog_ext.cog_subcommand(**params["blackjack_hilo"]) async def blackjack_hilo(self, ctx): """Get the hilo value for the deck in blackjack.""" diff --git a/gwendolyn/cogs/misc_cog.py b/gwendolyn/cogs/misc_cog.py index 5042757..1b76764 100644 --- a/gwendolyn/cogs/misc_cog.py +++ b/gwendolyn/cogs/misc_cog.py @@ -1,6 +1,7 @@ """Contains the MiscCog, which deals with miscellaneous commands.""" from discord.ext import commands # Has the cog class from discord_slash import cog_ext # Used for slash commands +from discord_slash.context import SlashContext from gwendolyn.utils import get_params # pylint: disable=import-error @@ -19,77 +20,77 @@ class MiscCog(commands.Cog): self.nerd_shit = bot.other.nerd_shit @cog_ext.cog_slash(**params["ping"]) - async def ping(self, ctx): + async def ping(self, ctx: SlashContext): """Send the bot's latency.""" await ctx.send(f"Pong!\nLatency is {round(self.bot.latency * 1000)}ms") @cog_ext.cog_slash(**params["stop"]) - async def stop(self, ctx): + async def stop(self, ctx: SlashContext): """Stop the bot.""" await self.bot.stop(ctx) @cog_ext.cog_slash(**params["help"]) - async def help_command(self, ctx, command=""): + async def help_command(self, ctx: SlashContext, command=""): """Get help for commands.""" await self.bot.other.helpFunc(ctx, command) @cog_ext.cog_slash(**params["thank"]) - async def thank(self, ctx): + async def thank(self, ctx: SlashContext): """Thank the bot.""" await ctx.send("You're welcome :blush:") @cog_ext.cog_slash(**params["hello"]) - async def hello(self, ctx): + async def hello(self, ctx: SlashContext): """Greet the bot.""" await self.bot.other.helloFunc(ctx) @cog_ext.cog_slash(**params["roll"]) - async def roll(self, ctx, dice="1d20"): + async def roll(self, ctx: SlashContext, dice="1d20"): """Roll dice.""" await self.bot.other.rollDice(ctx, dice) @cog_ext.cog_slash(**params["image"]) - async def image(self, ctx): + async def image(self, ctx: SlashContext): """Get a random image from Bing.""" await self.bot.other.imageFunc(ctx) @cog_ext.cog_slash(**params["movie"]) - async def movie(self, ctx): + async def movie(self, ctx: SlashContext): """Get a random movie from the Plex server.""" await self.bot.other.movieFunc(ctx) @cog_ext.cog_slash(**params["name"]) - async def name(self, ctx): + async def name(self, ctx: SlashContext): """Generate a random name.""" await self.generators.nameGen(ctx) @cog_ext.cog_slash(**params["tavern"]) - async def tavern(self, ctx): + async def tavern(self, ctx: SlashContext): """Generate a random tavern name.""" await self.generators.tavernGen(ctx) @cog_ext.cog_slash(**params["wiki"]) - async def wiki(self, ctx, wiki_page=""): + async def wiki(self, ctx: SlashContext, wiki_page=""): """Get a page on a fandom wiki.""" await self.bot.other.findWikiPage(ctx, wiki_page) @cog_ext.cog_slash(**params["add_movie"]) - async def add_movie(self, ctx, movie): + async def add_movie(self, ctx: SlashContext, movie): """Search for a movie and add it to the Plex server.""" await self.plex.request_movie(ctx, movie) @cog_ext.cog_slash(**params["add_show"]) - async def add_show(self, ctx, show): + async def add_show(self, ctx: SlashContext, show): """Search for a show and add it to the Plex server.""" await self.plex.request_show(ctx, show) @cog_ext.cog_slash(**params["downloading"]) - async def downloading(self, ctx, parameters="-d"): + async def downloading(self, ctx: SlashContext, parameters="-d"): """Get the current downloading torrents.""" await self.plex.downloading(ctx, parameters) @cog_ext.cog_slash(**params["wolf"]) - async def wolf(self, ctx, query): + async def wolf(self, ctx: SlashContext, query): """Perform a search on Wolfram Alpha.""" await self.nerd_shit.wolfSearch(ctx, query) diff --git a/gwendolyn/exceptions.py b/gwendolyn/exceptions.py new file mode 100644 index 0000000..25ced8f --- /dev/null +++ b/gwendolyn/exceptions.py @@ -0,0 +1,9 @@ +class GameNotInDatabase(Exception): + def __init__(self, game: str, channel: str): + self.message = f"There is no {game} game in channel {channel}" + super().__init__(self.message) + +class InvalidInteraction(Exception): + def __init__(self, custom_id: str, decoded: str): + self.message = f"{custom_id = }, {decoded = }" + super().__init__(self.message) diff --git a/gwendolyn/funcs/games/blackjack.py b/gwendolyn/funcs/games/blackjack.py index 22bc931..e3c307d 100644 --- a/gwendolyn/funcs/games/blackjack.py +++ b/gwendolyn/funcs/games/blackjack.py @@ -12,7 +12,10 @@ import math # Used for flooring decimal numbers import datetime # Used to generate the game id import asyncio # Used for sleeping -from discord_slash.context import SlashContext # Used for typehints +from discord_slash.context import InteractionContext as IntCont # Used for +# typehints +from discord_slash.context import ComponentContext +from discord.abc import Messageable from PIL import Image from gwendolyn.utils import replace_multiple @@ -38,15 +41,10 @@ def _is_round_done(game: dict): 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 + return all( + hand["hit"] or hand["standing"] + for user_hands in game["user hands"].values() for hand in user_hands + ) class Blackjack(CardGame): """ @@ -54,53 +52,51 @@ class Blackjack(CardGame): *Methods* --------- - 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) + hit(ctx: IntCont, hand_number: int = 0) + double(ctx: IntCont, hand_number: int = 0) + stand(ctx: IntCont, hand_number: int = 0) + split(ctx: IntCont, hand_number: int = 0) + enter_game(ctx: IntCont, bet: int) + start(ctx: IntCont) + hilo(ctx: IntCont) + shuffle(ctx: IntCont) + cards(ctx: IntCont) """ def __init__(self, bot): """Initialize the class.""" - super().__init__(bot) - self.decks_used = 4 - self.game_name = "blackjack" - self.draw = DrawBlackjack(bot, self) + super().__init__(bot, "blackjack", DrawBlackjack(bot, self), 4) + default_buttons = ["Hit", "Stand", "Double", "Split"] + self.default_buttons = [(i, [i, "0"], 1) for i in default_buttons] - async def _test_valid_command(self, game: dict, user: str, - hand_number: int, ctx: SlashContext): + async def _test_command(self, game: dict, user: str, command: str, + hand_number: int, ctx: IntCont): #pylint:disable=too-many-arguments 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") + if user not in game["user hands"]: + self.bot.log(f"They tried to {command} without being in the game") + send_msg = f"You have to enter the game before you can {command}" 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") + self.bot.log(f"They tried to {command} with a hand they don't have") + send_msg = "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!") + self.bot.log(f"They tried to {command} on the 0th round") + send_msg = "You haven't seen your cards yet!" + elif len(game["user hands"][user]) > 1 and hand_number == 0: + self._get_hand_number(ctx, command, game["user hands"][user]) + return False 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") + send_msg = "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") + send_msg = "You're already standing" else: valid_command = True + if not valid_command: + await ctx.send(send_msg, hidden=True) + return valid_command def _shuffle_cards(self, channel: str): @@ -158,12 +154,8 @@ class Blackjack(CardGame): def _draw_card(self, channel: str): drawn_card = super()._draw_card(channel) - value = self._calc_hand_value([drawn_card]) - - if value <= 6: - self._update_document(channel, {"$inc": {"hilo": 1}}, "hilo") - elif value >= 10: - self._update_document(channel, {"$inc": {"hilo": -1}}, "hilo") + hilo_value = min(1,-1-((self._calc_hand_value([drawn_card])-10)//3)) + self._update_document(channel, {"$inc": {"hilo": hilo_value}}, "hilo") return drawn_card @@ -182,14 +174,13 @@ class Blackjack(CardGame): Whether the dealer is done drawing cards. """ game = self.access_document(channel) - - done = False dealer_hand = game["dealer hand"] if self._calc_hand_value(dealer_hand) < 17: dealer_hand.append(self._draw_card(channel)) dealer_updater = {"$set": {"dealer hand": dealer_hand}} self._update_document(channel, dealer_updater) + done = False else: done = True @@ -219,31 +210,31 @@ class Blackjack(CardGame): self.bot.log("Continuing blackjack game") game = self.access_document(channel) - done = False - self._update_document(channel, {"$inc": {"round": 1}}) - + message = self.long_strings["Blackjack all players standing"] all_standing = True pre_all_standing = True - message = self.long_strings["Blackjack all players standing"] + done = False if game["all standing"]: self.bot.log("All are standing") - done = self._dealer_draw(channel) message = "The dealer draws a card." self.bot.log("Testing if all are standing") 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}} + hand_updater = {"$set": {"user hands."+user: standing_test[0]}} self._update_document(channel, hand_updater) + if not standing_test[1]: + all_standing = False + if not standing_test[2]: + pre_all_standing = False + if all_standing: self._update_document(channel, {"$set": {"all standing": True}}) - if all_standing: if done: message = "The dealer is done drawing cards" @@ -304,7 +295,7 @@ class Blackjack(CardGame): return user_hands, all_standing, pre_all_standing - def _blackjack_finish(self, channel: str): + def _blackjack_finish(self, channel: Messageable): """ Generate the winnings message after the blackjack game ends. @@ -320,7 +311,7 @@ class Blackjack(CardGame): """ final_winnings = "*Final Winnings:*\n" - game = self.access_document(channel) + game = self.access_document(str(channel.id)) for user in game["user hands"]: winnings, net_winnings, reason = self._calc_winning( @@ -333,7 +324,7 @@ class Blackjack(CardGame): user_name = self.bot.database_funcs.get_name(user) if winnings < 0: - final_winnings += "{user_name} lost" + final_winnings += f"{user_name} lost" else: final_winnings += f"{user_name} won" @@ -342,11 +333,11 @@ class Blackjack(CardGame): else: final_winnings += f" {abs(winnings)} GwendoBucks" - winnings_text += f" {reason}\n" + final_winnings += f" {reason}\n" self.bot.money.addMoney(user, net_winnings) - self._delete_document(channel) + await self._end_game(channel) return final_winnings @@ -386,32 +377,27 @@ class Blackjack(CardGame): net_winnings = 0 for hand in user_hands: - bet = hand["bet"] - winnings += -1 * bet + winnings += -1 * hand["bet"] + hand_value = self._calc_hand_value(hand["hand"]) - 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: - 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)" + states = [ + (dealer_blackjack, "dealer blackjack", 0), + (hand["blackjack"], "blackjack", math.floor(2.5 * hand["bet"])), + (hand["busted"], "busted", 0), + (dealer_busted, dealer_busted, 2*hand["bet"]), + (hand_value == dealer_value, "pushed", hand["bet"]), + (hand_value > dealer_value, "highest value", 2 * hand["bet"]), + (True, "highest value", 0) + ] + + for state in states: + if state[0]: + reason += f"({state[1]})" + net_winnings += state[2] + break winnings += net_winnings - return winnings, net_winnings, reason[1:] + return winnings, net_winnings, reason async def _blackjack_loop(self, channel, game_round: int, game_id: str): """ @@ -437,22 +423,17 @@ class Blackjack(CardGame): if not game_done: await self._delete_old_image(channel) - await self._send_image(channel) + buttons = None if all_standing else self.default_buttons + await self._send_image(channel, buttons) - if all_standing: - await asyncio.sleep(5) - else: - await asyncio.sleep(120) + await asyncio.sleep([120,5][all_standing]) - game = self.access_document(str(channel.id)) - - if game is None: + if not self._test_document(str(channel.id)): self.bot.log(f"Ending loop on round {game_round}", str(channel.id)) return - real_round = game["round"] or -1 - real_id = game["game_id"] or -1 - if game_round != real_round or game_id != real_id: + game = self.access_document(str(channel.id)) + if game_round != game["round"] or game_id != game["game_id"]: self.bot.log(f"Ending loop on round {game_round}", str(channel.id)) return @@ -461,17 +442,26 @@ class Blackjack(CardGame): 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) + await channel.send(self._blackjack_finish(channel)) + async def _get_hand_number(self, ctx: IntCont, command: str, + hands_amount: int): + buttons = [ + (str(i+1), [command, "0", str(i+1)], 1) for i in range(hands_amount) + ] + await ctx.send( + f"Which hand do you want to {command}?", + hidden=True, + components=self._get_action_rows(buttons) + ) - async def hit(self, ctx: SlashContext, hand_number: int = 0): + async def _hit(self, ctx: IntCont, hand_number: int = 0): """ Hit on a hand. *Parameters* ------------ - ctx: SlashContext + ctx: IntCont The context of the command. hand_number: int = 1 The number of the hand to hit. @@ -481,10 +471,7 @@ class Blackjack(CardGame): user = f"#{ctx.author.id}" game = self.access_document(channel) - - valid_command = self._test_valid_command(game, user, hand_number, ctx) - - if not valid_command: + if not await self._test_command(game, user, "hit", hand_number, ctx): return hand = game["user hands"][user][max(hand_number-1,0)] @@ -511,13 +498,13 @@ class Blackjack(CardGame): ctx.channel, game["round"]+1, game_id ) - async def double(self, ctx: SlashContext, hand_number: int = 0): + async def _double(self, ctx: IntCont, hand_number: int = 0): """ Double a hand. *Parameters* ------------ - ctx: SlashContext + ctx: IntCont The context of the command. hand_number: int = 0 The number of the hand to double. @@ -530,17 +517,15 @@ class Blackjack(CardGame): balance = self.bot.money.checkBalance(user) - valid_command = self._test_valid_command(game, user, hand_number, ctx) - - if not valid_command: + if not await self._test_command(game, user, "double", hand_number, ctx): return 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") + await ctx.send("You can only double on the first round") + self.bot.log("They tried to double after round 1") 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") + await ctx.send("You can't double when you don't have enough money") + self.bot.log("They tried to double without having enough money") else: hand = game["user hands"][user][max(hand_number-1,0)] self.bot.money.addMoney(user, -1 * hand["bet"]) @@ -573,13 +558,13 @@ class Blackjack(CardGame): ) - async def stand(self, ctx: SlashContext, hand_number: int = 0): + async def _stand(self, ctx: IntCont, hand_number: int = 0): """ Stand on a hand. *Parameters* ------------ - ctx: SlashContext + ctx: IntCont The context of the command. hand_number: int = 0 The number of the hand to stand on. @@ -590,9 +575,7 @@ class Blackjack(CardGame): game = self.access_document(channel) - valid_command = self._test_valid_command(game, user, hand_number, ctx) - - if not valid_command: + if not await self._test_command(game, user, "stand", hand_number, ctx): return hand = game["user hands"][user][max(hand_number-1,0)] @@ -613,13 +596,13 @@ class Blackjack(CardGame): await self._blackjack_loop(ctx.channel, game["round"]+1, game_id) - async def split(self, ctx: SlashContext, hand_number: int = 0): + async def _split(self, ctx: IntCont, hand_number: int = 0): """ Split a hand. *Parameters* ------------ - ctx: SlashContext + ctx: IntCont The context of the command. hand_number: int = 0 The number of the hand to split. @@ -630,10 +613,7 @@ class Blackjack(CardGame): game = self.access_document(channel) - - valid_command = self._test_valid_command(game, user, hand_number, ctx) - - if not valid_command: + if not await self._test_command(game, user, "split", hand_number, ctx): return old_hand = game["user hands"][user][max(hand_number-1,0)] @@ -644,7 +624,7 @@ class Blackjack(CardGame): 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: + 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"]: @@ -689,13 +669,13 @@ class Blackjack(CardGame): ctx.channel, game["round"]+1, game_id ) - async def enter_game(self, ctx: SlashContext, bet: int): + async def enter_game(self, ctx: IntCont, bet: int): """ Enter the blackjack game. *Parameters* ------------ - ctx: SlashContext + ctx: IntCont The context of the command. bet: int The bet to enter with. @@ -709,24 +689,21 @@ class Blackjack(CardGame): self.bot.log(f"{user_name} is trying to join the Blackjack game") - if game is None: - send_message = "There is no game going on in this channel" - log_message = send_message - elif user in game["user hands"]: - send_message = "You're already in the game!" - log_message = "They're already in the game" + if user in game["user hands"]: + await ctx.send("You're already in the game!") + self.bot.log("They're already in the game") elif len(game["user hands"]) >= 5: - send_message = "There can't be more than 5 players in a game" - log_message = "There were already 5 players in the game" + await ctx.send("There can't be more than 5 players in a game") + self.bot.log("There were already 5 players in the game") elif game["round"] != 0: - send_message = "The table is no longer taking bets" - log_message = "They tried to join after the game begun" + await ctx.send("The table is no longer taking bets") + self.bot.log("They tried to join after the game begun") elif bet < 0: - send_message = "You can't bet a negative amount" - log_message = "They tried to bet a negative amount" + await ctx.send("You can't bet a negative amount") + self.bot.log("They tried to bet a negative amount") elif self.bot.money.checkBalance(user) < bet: - send_message = "You don't have enough GwendoBucks" - log_message = "They didn't have enough GwendoBucks" + await ctx.send("You don't have enough GwendoBucks") + self.bot.log("They didn't have enough GwendoBucks") else: self.bot.money.addMoney(user, -1 * bet) player_hand = [self._draw_card(channel) for _ in range(2)] @@ -742,19 +719,16 @@ class Blackjack(CardGame): self._update_document(channel, updater) send_message = "{} entered the game with a bet of {} GwendoBucks" - send_message = send_message.format(user_name, bet) - log_message = send_message + await ctx.send(send_message.format(user_name, bet)) + self.bot.log(send_message.format(user_name, bet)) - self.bot.log(log_message) - await ctx.send(send_message) - - async def start(self, ctx: SlashContext): + async def start(self, ctx: IntCont): """ Start a blackjack game. *Parameters* ------------ - ctx: SlashContext + ctx: IntCont The context of the command. """ await self.bot.defer(ctx) @@ -764,21 +738,18 @@ class Blackjack(CardGame): self.bot.log("Starting blackjack game") await ctx.send("Starting a new game of blackjack") cards_left = 0 - cards = self.access_document(channel, "cards") - if cards is not None: + if self._test_document(channel, "cards"): + cards = self.access_document(channel, "cards") cards_left = len(cards["cards"]) # Shuffles if not enough cards if cards_left < blackjack_min_cards: self._shuffle_cards(channel) - self.bot.log("Shuffling the blackjack deck...", channel) await ctx.channel.send("Shuffling the deck...") - game = self.access_document(channel) - self.bot.log(f"Trying to start a blackjack game in {channel}") - if game is not None: + if self._test_document(channel): await ctx.channel.send(self.long_strings["Blackjack going on"]) self.bot.log("There was already a game going on") return @@ -794,14 +765,18 @@ class Blackjack(CardGame): } await ctx.channel.send(self.long_strings["Blackjack started"]) - await self._start_new(ctx.channel, new_game) + labels = [0] + [10**i for i in range(4)] + betting_buttons = [ + (f"Bet {i}", ["bet", "0", str(i)], 1) for i in labels + ] + await self._start_new(ctx.channel, new_game, betting_buttons) 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)) + await ctx.channel.send(self._blackjack_finish(ctx.channel)) return game_id = game["game_id"] @@ -810,49 +785,60 @@ class Blackjack(CardGame): self.bot.log("start() calling _blackjack_loop()", channel) await self._blackjack_loop(ctx.channel, 1, game_id) - async def hilo(self, ctx: SlashContext): + async def hilo(self, ctx: IntCont): """ Get the hilo of the blackjack game. *Parameters* ------------ - ctx: SlashContext + ctx: IntCont The context of the command. """ - data = self.access_document(str(ctx.channel_id), "hilo") + data = self.access_document(str(ctx.channel_id), "hilo", False) hilo = data["hilo"] if data else 0 await ctx.send(f"Hi-lo value: {hilo}", hidden=True) - async def shuffle(self, ctx: SlashContext): + async def shuffle(self, ctx: IntCont): """ Shuffle the cards used for blackjack. *Parameters* ------------ - ctx: SlashContext + ctx: IntCont The context of the command. """ 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: SlashContext): + async def cards(self, ctx: IntCont): """ Get how many cards are left for blackjack. *Parameters* ------------ - ctx: SlashContext + ctx: IntCont The context of the command. """ - cards = self.access_document(str(ctx.channel_id), "cards") + cards = self.access_document(str(ctx.channel_id), "cards", False) cards_left = len(cards["cards"]) if cards else 0 - decks_left = round(cards_left/52, 1) + decks_left = round(cards_left/52, 1) if cards else 0 send_message = f"Cards left:\n{cards_left} cards, {decks_left} decks" await ctx.send(send_message, hidden=True) + async def decode_interaction(self, ctx: ComponentContext, info: list[str]): + if info[0].lower() == "bet": + await self.enter_game(ctx, int(info[2])) + elif info[0].lower() == "hit": + await self._hit(ctx, int(info[2]) if len(info) > 2 else 0) + elif info[0].lower() == "stand": + await self._stand(ctx, int(info[2]) if len(info) > 2 else 0) + elif info[0].lower() == "double": + await self._double(ctx, int(info[2]) if len(info) > 2 else 0) + elif info[0].lower() == "split": + await self._split(ctx, int(info[2]) if len(info) > 2 else 0) + class DrawBlackjack(CardDrawer): """ @@ -882,7 +868,7 @@ class DrawBlackjack(CardDrawer): def _draw_dealer_hand(self, game: dict, table: Image.Image): dealer_hand = self._draw_hand( game["dealer hand"], - game["all standing"], + not game["all standing"], game["dealer busted"] if game["all standing"] else False, game["dealer blackjack"] if game["all standing"] else False ) @@ -972,10 +958,9 @@ class DrawBlackjack(CardDrawer): colors = [BLACK, WHITE, last_color] text_image = self._draw_shadow_text(text, colors, font_size) - background.paste(text_image, text_position, text_image) - width = background.size[0] text_width = self._get_font(font_size).getsize(text)[0] text_position = (int(width/2)-int(text_width/2), 85) + background.paste(text_image, text_position, text_image) return background diff --git a/gwendolyn/funcs/games/connect_four.py b/gwendolyn/funcs/games/connect_four.py index 6dcb726..40c9eb1 100644 --- a/gwendolyn/funcs/games/connect_four.py +++ b/gwendolyn/funcs/games/connect_four.py @@ -22,12 +22,46 @@ create_actionrow) from discord_slash.model import ButtonStyle from gwendolyn.utils import encode_id +from .game_base import BoardGame ROWCOUNT = 6 COLUMNCOUNT = 7 +def _encode_board_string(board: list): + string = [str(i) for row in board for i in row] -class ConnectFour(): + while len(string) > 0 and string[0] == "0": + string = string[1:] + + if string == "": + string = "0" + + dec = 0 + for i, digit in enumerate(string[::-1]): + dec += (3**i)*int(digit) + + return str(dec) + +def _decode_board_string(board_string: str): + dec = int(board_string) + string = [] + while dec: + string.append(str(dec % 3)) + dec = dec // 3 + + while len(string) < ROWCOUNT * COLUMNCOUNT: + string.append("0") + + string = string[::-1] + + board = [ + [int(x) for x in string[i*COLUMNCOUNT:i*COLUMNCOUNT+COLUMNCOUNT]] + for i in range(ROWCOUNT)] + + return board + + +class ConnectFour(BoardGame): """ Deals with connect four commands and logic. @@ -41,8 +75,7 @@ class ConnectFour(): def __init__(self, bot): """Initialize the class.""" - self.bot = bot - self.draw = DrawConnectFour(bot) + super().__init__(bot, "connectfour", DrawConnectFour(bot)) self.get_name = self.bot.database_funcs.get_name # pylint: disable=invalid-name self.AISCORES = { @@ -57,39 +90,6 @@ class ConnectFour(): } # pylint: enable=invalid-name - def _encode_board_string(self, board: list): - string = [str(i) for row in board for i in row] - - while len(string) > 0 and string[0] == "0": - string = string[1:] - - if string == "": - string = "0" - - dec = 0 - for i, digit in enumerate(string[::-1]): - dec += (3**i)*int(digit) - - return str(dec) - - def _decode_board_string(self, board_string: str): - dec = int(board_string) - string = [] - while dec: - string.append(str(dec % 3)) - dec = dec // 3 - - while len(string) < ROWCOUNT * COLUMNCOUNT: - string.append("0") - - string = string[::-1] - - board = [ - [int(x) for x in string[i*COLUMNCOUNT:i*COLUMNCOUNT+COLUMNCOUNT]] - for i in range(ROWCOUNT)] - - return board - async def start(self, ctx: SlashContext, opponent: Union[int, discord.User]): """ @@ -106,128 +106,90 @@ class ConnectFour(): searches when minimaxing. """ await self.bot.defer(ctx) - user = ctx.author.id channel = str(ctx.channel_id) - started_game = False - can_start = True + opponent_info = self._test_opponent(ctx, opponent) + if not opponent_info: + return - if isinstance(opponent, int): - # Opponent is Gwendolyn - if opponent in range(1, 6): - difficulty = int(opponent) - difficulty_text = f" with difficulty {difficulty}" - opponent = self.bot.user.id - else: - send_message = "Difficulty doesn't exist" - log_message = "They challenged a difficulty that doesn't exist" - can_start = False - elif isinstance(opponent, discord.User): - if opponent.bot: - # User has challenged a bot - if opponent == self.bot.user: - # It was Gwendolyn - difficulty = 3 - difficulty_text = f" with difficulty {difficulty}" - opponent = self.bot.user.id - else: - send_message = "You can't challenge a bot!" - log_message = "They tried to challenge a bot" - can_start = False - else: - # Opponent is another player - if ctx.author != opponent: - opponent = opponent.id - difficulty = 5 - difficulty_text = "" - else: - send_message = "You can't play against yourself" - log_message = "They tried to play against themself" - can_start = False + difficulty = opponent_info[0] + difficulty_text = opponent_info[1] - if can_start: - board = [[0 for _ in range(COLUMNCOUNT)] for _ in range(ROWCOUNT)] - players = [user, opponent] - random.shuffle(players) + board = [[0 for _ in range(COLUMNCOUNT)] for _ in range(ROWCOUNT)] + players = [ctx.author.id, opponent] + random.shuffle(players) - self.draw.draw_image(channel, board, 0, [0,0], "", players) + self.draw.draw_image(channel, board, players, [0, [0,0], ""], players) - gwendolyn_turn = (players[0] == self.bot.user.id) - started_game = True + opponent_name = self.get_name(f"#{opponent}") + turn_name = self.get_name(f"#{players[0]}") - opponent_name = self.get_name(f"#{opponent}") - turn_name = self.get_name(f"#{players[0]}") - - started_text = "Started game against {}{}.".format( - opponent_name, - difficulty_text - ) - turn_text = f"It's {turn_name}'s turn" - send_message = f"{started_text} {turn_text}" - log_message = "They started a game" - - self.bot.log(log_message) - await ctx.send(send_message) + started_text = "Started game against {}{}.".format( + opponent_name, + difficulty_text + ) + turn_text = f"It's {turn_name}'s turn" + await ctx.send(f"{started_text} {turn_text}") + self.bot.log("They started a game") # Sets the whole game in motion - if started_game: - boards_path = "gwendolyn/resources/games/connect_four_boards/" - file_path = f"{boards_path}board{ctx.channel_id}.png" - image_message = await ctx.send(file=discord.File(file_path)) - - board_string = self._encode_board_string(board) - if gwendolyn_turn: - await self._connect_four_ai( - ctx, board_string, players, difficulty, image_message.id - ) - else: - buttons = [] - for i in range(7): - custom_id = encode_id( - [ - "connectfour", - "place", - str(players[0]), - board_string, - str(i), - str(players[0]), - str(players[1]), - str(difficulty), - str(image_message.id) - ] - ) - buttons.append(create_button( - style=ButtonStyle.blue, - label=str(i+1), - custom_id=custom_id, - disabled=(board[0][i] != 0) - )) + boards_path = "gwendolyn/resources/games/connect_four_boards/" + file_path = f"{boards_path}board{ctx.channel_id}.png" + image_message = await ctx.send(file=discord.File(file_path)) + board_string = _encode_board_string(board) + if (players[0] == self.bot.user.id): + await self._connect_four_ai( + ctx, board_string, players, difficulty, image_message.id + ) + else: + buttons = [] + for i in range(7): custom_id = encode_id( [ "connectfour", - "end", + "place", + str(players[0]), + board_string, + str(i), str(players[0]), str(players[1]), + str(difficulty), str(image_message.id) ] ) buttons.append(create_button( - style=ButtonStyle.red, - label="Surrender", - custom_id=custom_id + style=ButtonStyle.blue, + label=str(i+1), + custom_id=custom_id, + disabled=(board[0][i] != 0) )) - action_rows = [] - for x in range(((len(buttons)-1)//4)+1): - row_buttons = buttons[ - (x*4):(min(len(buttons),x*4+4)) - ] - action_rows.append(create_actionrow(*row_buttons)) + custom_id = encode_id( + [ + "connectfour", + "end", + str(players[0]), + str(players[1]), + str(image_message.id) + ] + ) + buttons.append(create_button( + style=ButtonStyle.red, + label="Surrender", + custom_id=custom_id + )) - await image_message.edit( - components=action_rows - ) + action_rows = [] + for x in range(((len(buttons)-1)//4)+1): + row_buttons = buttons[ + (x*4):(min(len(buttons),x*4+4)) + ] + action_rows.append(create_actionrow(*row_buttons)) + + await image_message.edit( + components=action_rows + ) async def place_piece(self, ctx: ComponentContext, board_string: str, column: int, players: list[int], difficulty: int, @@ -246,7 +208,7 @@ class ConnectFour(): user_name = self.get_name(f"#{placer}") placed_piece = False - board = self._decode_board_string(board_string) + board = _decode_board_string(board_string) board = self._place_on_board(board, player_number, column) if board is None: @@ -300,7 +262,7 @@ class ConnectFour(): if game_won: self._end_game(winner, players, difficulty) else: - board_string = self._encode_board_string(board) + board_string = _encode_board_string(board) if gwendolyn_turn: await self._connect_four_ai( ctx, board_string, players, difficulty, image_message.id @@ -513,7 +475,7 @@ class ConnectFour(): self.bot.log("Figuring out best move") - board = self._decode_board_string(board_string) + board = _decode_board_string(board_string) player = players.index(self.bot.user.id)+1 scores = [-math.inf for _ in range(COLUMNCOUNT)] @@ -797,8 +759,8 @@ class DrawConnectFour(): # pylint: enable=invalid-name # Draws the whole thing - def draw_image(self, channel: str, board: list, winner: int, - win_coordinates: list, win_direction: str, players: list): + def draw_image(self, channel: str, board: list, players: list, + win_info: list): """ Draw an image of the connect four board. @@ -815,8 +777,8 @@ class DrawConnectFour(): self._draw_pieces(drawer, board) - if winner != 0: - self._draw_win(background, win_coordinates, win_direction) + if win_info[0] != 0: + self._draw_win(background, win_info[1], win_info[2]) self._draw_footer(drawer, players) diff --git a/gwendolyn/funcs/games/game_base.py b/gwendolyn/funcs/games/game_base.py index 3a2a1c4..b84e0ce 100644 --- a/gwendolyn/funcs/games/game_base.py +++ b/gwendolyn/funcs/games/game_base.py @@ -1,28 +1,80 @@ """Base class for the games.""" import random -from shutil import copyfile +from typing import Union -from discord import File +from discord import File, User from discord.abc import Messageable +from discord_slash.utils.manage_components import (create_button, + create_actionrow) +from discord_slash.context import InteractionContext as IntCont from PIL import ImageFont, Image, ImageDraw +from gwendolyn.exceptions import GameNotInDatabase +from gwendolyn.utils import encode_id + class GameBase(): """The base class for the games.""" - def __init__(self, bot): + def __init__(self, bot, game_name: int, drawer): """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) + self.game_name = game_name + self.draw = drawer - def access_document(self, channel: str, collection_name: str="games"): + def _get_action_rows(self, buttons: list[tuple[str, list]]): + self.bot.log("Generation action rows") + button_objects = [] + for label, data, style in buttons: + custom_id = encode_id([self.game_name] + data) + button_objects.append(create_button( + style=style, + label=label, + custom_id=custom_id + )) + + action_rows = [] + for i in range(((len(button_objects)-1)//5)+1): + action_rows.append(create_actionrow(*button_objects[i*5:i*5+5])) + + return action_rows + + async def _send_image(self, channel: Messageable, + buttons: list[tuple[str, list]] = None, delete=True): + self.draw.draw(str(channel.id)) + file_path = f"{self.resources}images/{self.game_name}{channel.id}.png" + old_image = await channel.send( + file=File(file_path), delete_after=120 if delete else None) + + if buttons is not None and len(buttons) < 25: + await old_image.edit(components = self._get_action_rows(buttons)) + + return old_image + +class DatabaseGame(GameBase): + """The base class for the games.""" + + def __init__(self, bot, game_name, drawer): + """Initialize the class.""" + super().__init__(bot, game_name, drawer) + self.database = self.bot.database + self.old_images_path = f"{self.resources}old_images/{self.game_name}" + + def access_document(self, channel: str, collection_name: str="games", + raise_missing_error: bool=True): collection = self.bot.database[f"{self.game_name} {collection_name}"] - return collection.find_one({"_id": channel}) + game = collection.find_one({"_id": channel}) + if game is None and raise_missing_error: + raise GameNotInDatabase(self.game_name, channel) + + return game + + def _test_document(self, channel: str, collection_name: str="games"): + collection = self.bot.database[f"{self.game_name} {collection_name}"] + game = collection.find_one({"_id": channel}) + return game is not None def _update_document(self, channel: str, updater: dict, collection_name: str="games"): @@ -46,28 +98,29 @@ class GameBase(): 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)) + async def _send_image(self, channel: Messageable, + buttons: list[tuple[str, list]] = None, delete=True): + old_image = super()._send_image(channel, buttons, delete) with open(self.old_images_path + str(channel.id), "w") as file_pointer: file_pointer.write(str(old_image.id)) - async def _start_new(self, channel: Messageable, new_game: dict): + async def _start_new(self, channel: Messageable, new_game: dict, + buttons: list[tuple[str, list]] = None): 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, buttons) - await self._send_image(channel) + async def _end_game(self, channel: Messageable): + await self._delete_old_image(channel) + await self._send_image(channel, delete=False) + self._delete_document(str(channel.id)) -class CardGame(GameBase): +class CardGame(DatabaseGame): """The class for card games.""" - def __init__(self, bot): + def __init__(self, bot, game_name, drawer, deck_used): """Initialize the class.""" - super().__init__(bot) - self.decks_used = 1 + super().__init__(bot, game_name, drawer) + self.decks_used = deck_used def _shuffle_cards(self, channel: str): self.bot.log(f"Shuffling cards for {self.game_name}") @@ -99,6 +152,43 @@ class CardGame(GameBase): return drawn_card +class BoardGame(GameBase): + def _test_opponent(self, ctx: IntCont, opponent: Union[int, User]): + if isinstance(opponent, int): + # Opponent is Gwendolyn + if opponent in range(1, 6): + difficulty = int(opponent) + difficulty_text = f" with difficulty {difficulty}" + opponent = self.bot.user.id + else: + await ctx.send("Difficulty doesn't exist") + self.bot.log("They challenged a difficulty that doesn't exist") + return False + elif isinstance(opponent, User): + if opponent.bot: + # User has challenged a bot + if opponent == self.bot.user: + # It was Gwendolyn + difficulty = 3 + difficulty_text = f" with difficulty {difficulty}" + opponent = self.bot.user.id + else: + await ctx.send("You can't challenge a bot!") + self.bot.log("They tried to challenge a bot") + return False + else: + # Opponent is another player + if ctx.author != opponent: + opponent = opponent.id + difficulty = 5 + difficulty_text = "" + else: + await ctx.send("You can't play against yourself") + self.bot.log("They tried to play against themself") + return False + + return difficulty, difficulty_text + class BaseDrawer(): """Class for drawing games.""" def __init__(self, bot, game: GameBase): diff --git a/gwendolyn/funcs/games/hangman.py b/gwendolyn/funcs/games/hangman.py index 83c3dfe..0c22157 100644 --- a/gwendolyn/funcs/games/hangman.py +++ b/gwendolyn/funcs/games/hangman.py @@ -8,13 +8,13 @@ Deals with commands and logic for hangman games. DrawHangman() Draws the image shown to the player. """ +import os import datetime # Used for generating the game id import string # string.ascii_uppercase used import math # Used by DrawHangman(), mainly for drawing circles import random # Used to draw poorly import requests # Used for getting the word in Hangman.start() import discord # Used for discord.file and type hints -import os from discord_slash.utils.manage_components import (create_button, create_actionrow) @@ -50,10 +50,10 @@ class Hangman(): The parameters to pass to every api call. """ self.bot = bot - self.__draw = DrawHangman(bot) - self.__API_url = "https://api.wordnik.com/v4/words.json/randomWords?" # pylint: disable=invalid-name + self._draw = DrawHangman(bot) + self._API_url = "https://api.wordnik.com/v4/words.json/randomWords?" # pylint: disable=invalid-name api_key = self.bot.credentials["wordnik_key"] - self.__APIPARAMS = { # pylint: disable=invalid-name + self._APIPARAMS = { # pylint: disable=invalid-name "hasDictionaryDef": True, "minCorpusCount": 5000, "maxCorpusCount": -1, @@ -78,7 +78,7 @@ class Hangman(): word = "-" while "-" in word or "." in word: - response = requests.get(self.__API_url, params=self.__APIPARAMS) + response = requests.get(self._API_url, params=self._APIPARAMS) word = list(response.json()[0]["word"].upper()) self.bot.log("Found the word \""+"".join(word)+"\"") @@ -87,7 +87,7 @@ class Hangman(): remaining_letters = list(string.ascii_uppercase) - self.__draw.draw_image(game_id, 0, word, guessed, []) + self._draw.draw_image(game_id, 0, word, guessed, []) send_message = f"{ctx.author.display_name} started a game of hangman." @@ -219,7 +219,7 @@ class Hangman(): send_message = "Guessed {}. There were {} {}s in the word." send_message = send_message.format(guess, correct_guess, guess) - self.__draw.draw_image(game_id, misses, word, guessed, guessed_letters) + self._draw.draw_image(game_id, misses, word, guessed, guessed_letters) if misses == 6: send_message += self.bot.long_strings["Hangman lost game"] @@ -325,36 +325,36 @@ class DrawHangman(): FONT SMALLFONT """ - self.__bot = bot + self._bot = bot # pylint: disable=invalid-name - self.__CIRCLESIZE = 120 - self.__LINEWIDTH = 12 + self._CIRCLESIZE = 120 + self._LINEWIDTH = 12 - self.__BODYSIZE = 210 - self.__LIMBSIZE = 60 - self.__ARMPOSITION = 60 + self._BODYSIZE = 210 + self._LIMBSIZE = 60 + self._ARMPOSITION = 60 - self.__MANX = (self.__LIMBSIZE*2) - self.__MANY = (self.__CIRCLESIZE+self.__BODYSIZE+self.__LIMBSIZE) - MANPADDING = self.__LINEWIDTH*4 - self.__MANX += MANPADDING - self.__MANY += MANPADDING + self._MANX = (self._LIMBSIZE*2) + self._MANY = (self._CIRCLESIZE+self._BODYSIZE+self._LIMBSIZE) + MANPADDING = self._LINEWIDTH*4 + self._MANX += MANPADDING + self._MANY += MANPADDING - self.__LETTERLINELENGTH = 90 - self.__LETTERLINEDISTANCE = 30 + self._LETTERLINELENGTH = 90 + self._LETTERLINEDISTANCE = 30 - self.__GALLOWX, self.__GALLOWY = 360, 600 - self.__PHI = 1-(1 / ((1 + 5 ** 0.5) / 2)) + self._GALLOWX, self._GALLOWY = 360, 600 + self._PHI = 1-(1 / ((1 + 5 ** 0.5) / 2)) LETTERSIZE = 75 # Wrong guesses letter size WORDSIZE = 70 # Correct guesses letter size FONTPATH = "gwendolyn/resources/fonts/comic-sans-bold.ttf" - self.__FONT = ImageFont.truetype(FONTPATH, LETTERSIZE) - self.__SMALLFONT = ImageFont.truetype(FONTPATH, WORDSIZE) + self._FONT = ImageFont.truetype(FONTPATH, LETTERSIZE) + self._SMALLFONT = ImageFont.truetype(FONTPATH, WORDSIZE) # pylint: enable=invalid-name - def __deviate(self, pre_deviance: int, pre_deviance_accuracy: int, + def _deviate(self, pre_deviance: int, pre_deviance_accuracy: int, position_change: float, maxmin: int, max_acceleration: float): random_deviance = random.uniform(-position_change, position_change) @@ -371,14 +371,14 @@ class DrawHangman(): deviance = -maxmin return deviance, deviance_accuracy - def __bad_circle(self): - circle_padding = (self.__LINEWIDTH*3) - image_width = self.__CIRCLESIZE+circle_padding + def _bad_circle(self): + circle_padding = (self._LINEWIDTH*3) + image_width = self._CIRCLESIZE+circle_padding image_size = (image_width, image_width) background = Image.new("RGBA", image_size, color=(0, 0, 0, 0)) drawer = ImageDraw.Draw(background, "RGBA") - middle = (self.__CIRCLESIZE+(self.__LINEWIDTH*3))/2 + middle = (self._CIRCLESIZE+(self._LINEWIDTH*3))/2 deviance_x = 0 deviance_y = 0 deviance_accuracy_x = 0 @@ -387,96 +387,96 @@ class DrawHangman(): degrees_amount = 360 + random.randint(-10, 30) for degree in range(degrees_amount): - deviance_x, deviance_accuracy_x = self.__deviate( + deviance_x, deviance_accuracy_x = self._deviate( deviance_x, deviance_accuracy_x, - self.__LINEWIDTH/100, - self.__LINEWIDTH, + self._LINEWIDTH/100, + self._LINEWIDTH, 0.03 ) - deviance_y, deviance_accuracy_y = self.__deviate( + deviance_y, deviance_accuracy_y = self._deviate( deviance_y, deviance_accuracy_y, - self.__LINEWIDTH/100, - self.__LINEWIDTH, + self._LINEWIDTH/100, + self._LINEWIDTH, 0.03 ) radians = math.radians(degree+start) - circle_x = (math.cos(radians) * (self.__CIRCLESIZE/2)) - circle_y = (math.sin(radians) * (self.__CIRCLESIZE/2)) + circle_x = (math.cos(radians) * (self._CIRCLESIZE/2)) + circle_y = (math.sin(radians) * (self._CIRCLESIZE/2)) - position_x = middle + circle_x - (self.__LINEWIDTH/2) + deviance_x - position_y = middle + circle_y - (self.__LINEWIDTH/2) + deviance_y + position_x = middle + circle_x - (self._LINEWIDTH/2) + deviance_x + position_y = middle + circle_y - (self._LINEWIDTH/2) + deviance_y circle_position = [ (position_x, position_y), - (position_x+self.__LINEWIDTH, position_y+self.__LINEWIDTH) + (position_x+self._LINEWIDTH, position_y+self._LINEWIDTH) ] drawer.ellipse(circle_position, fill=(0, 0, 0, 255)) return background - def __bad_line(self, length: int, rotated: bool = False): + def _bad_line(self, length: int, rotated: bool = False): if rotated: - width, height = length+self.__LINEWIDTH*3, self.__LINEWIDTH*3 + width, height = length+self._LINEWIDTH*3, self._LINEWIDTH*3 else: - width, height = self.__LINEWIDTH*3, length+self.__LINEWIDTH*3 + width, height = self._LINEWIDTH*3, length+self._LINEWIDTH*3 background = Image.new("RGBA", (width, height), color=(0, 0, 0, 0)) drawer = ImageDraw.Draw(background, "RGBA") - possible_deviance = int(self.__LINEWIDTH/3) + possible_deviance = int(self._LINEWIDTH/3) deviance_x = random.randint(-possible_deviance, possible_deviance) deviance_y = 0 deviance_accuracy_x = 0 deviance_accuracy_y = 0 for pixel in range(length): - deviance_x, deviance_accuracy_x = self.__deviate( + deviance_x, deviance_accuracy_x = self._deviate( deviance_x, deviance_accuracy_x, - self.__LINEWIDTH/1000, - self.__LINEWIDTH, + self._LINEWIDTH/1000, + self._LINEWIDTH, 0.004 ) - deviance_y, deviance_accuracy_y = self.__deviate( + deviance_y, deviance_accuracy_y = self._deviate( deviance_y, deviance_accuracy_y, - self.__LINEWIDTH/1000, - self.__LINEWIDTH, + self._LINEWIDTH/1000, + self._LINEWIDTH, 0.004 ) if rotated: - position_x = self.__LINEWIDTH + pixel + deviance_x - position_y = self.__LINEWIDTH + deviance_y + position_x = self._LINEWIDTH + pixel + deviance_x + position_y = self._LINEWIDTH + deviance_y else: - position_x = self.__LINEWIDTH + deviance_x - position_y = self.__LINEWIDTH + pixel + deviance_y + position_x = self._LINEWIDTH + deviance_x + position_y = self._LINEWIDTH + pixel + deviance_y circle_position = [ (position_x, position_y), - (position_x+self.__LINEWIDTH, position_y+self.__LINEWIDTH) + (position_x+self._LINEWIDTH, position_y+self._LINEWIDTH) ] drawer.ellipse(circle_position, fill=(0, 0, 0, 255)) return background - def __draw_man(self, misses: int, seed: str): + def _draw_man(self, misses: int, seed: str): random.seed(seed) - man_size = (self.__MANX, self.__MANY) + man_size = (self._MANX, self._MANY) background = Image.new("RGBA", man_size, color=(0, 0, 0, 0)) if misses >= 1: - head = self.__bad_circle() - paste_x = (self.__MANX-(self.__CIRCLESIZE+(self.__LINEWIDTH*3)))//2 + head = self._bad_circle() + paste_x = (self._MANX-(self._CIRCLESIZE+(self._LINEWIDTH*3)))//2 paste_position = (paste_x, 0) background.paste(head, paste_position, head) if misses >= 2: - body = self.__bad_line(self.__BODYSIZE) - paste_x = (self.__MANX-(self.__LINEWIDTH*3))//2 - paste_position = (paste_x, self.__CIRCLESIZE) + body = self._bad_line(self._BODYSIZE) + paste_x = (self._MANX-(self._LINEWIDTH*3))//2 + paste_position = (paste_x, self._CIRCLESIZE) background.paste(body, paste_position, body) if misses >= 3: @@ -487,30 +487,30 @@ class DrawHangman(): random.seed(seed) for limb in limbs: - limb_drawing = self.__bad_line(self.__LIMBSIZE, True) - x_position = (self.__MANX-(self.__LINEWIDTH*3))//2 + limb_drawing = self._bad_line(self._LIMBSIZE, True) + x_position = (self._MANX-(self._LINEWIDTH*3))//2 if limb[1] == "a": rotation = random.randint(-45, 45) shift = math.sin(math.radians(rotation)) - line_length = self.__LIMBSIZE+(self.__LINEWIDTH*3) + line_length = self._LIMBSIZE+(self._LINEWIDTH*3) compensation = int(shift*line_length) limb_drawing = limb_drawing.rotate(rotation, expand=1) - y_position = self.__CIRCLESIZE + self.__ARMPOSITION + y_position = self._CIRCLESIZE + self._ARMPOSITION if limb == "ra": compensation = min(-compensation, 0) else: - x_position -= self.__LIMBSIZE + x_position -= self._LIMBSIZE compensation = min(compensation, 0) y_position += compensation else: rotation = random.randint(-15, 15) - y_position = self.__CIRCLESIZE+self.__BODYSIZE-self.__LINEWIDTH + y_position = self._CIRCLESIZE+self._BODYSIZE-self._LINEWIDTH if limb == "rl": limb_drawing = limb_drawing.rotate(rotation-45, expand=1) else: - x_position += -limb_drawing.size[0]+self.__LINEWIDTH*3 + x_position += -limb_drawing.size[0]+self._LINEWIDTH*3 limb_drawing = limb_drawing.rotate(rotation+45, expand=1) paste_position = (x_position, y_position) @@ -518,11 +518,11 @@ class DrawHangman(): return background - def __bad_text(self, text: str, big: bool, color: tuple = (0, 0, 0, 255)): + def _bad_text(self, text: str, big: bool, color: tuple = (0, 0, 0, 255)): if big: - font = self.__FONT + font = self._FONT else: - font = self.__SMALLFONT + font = self._SMALLFONT width, height = font.getsize(text) img = Image.new("RGBA", (width, height), color=(0, 0, 0, 0)) drawer = ImageDraw.Draw(img, "RGBA") @@ -530,59 +530,59 @@ class DrawHangman(): drawer.text((0, 0), text, font=font, fill=color) return img - def __draw_gallows(self): - gallow_size = (self.__GALLOWX, self.__GALLOWY) + def _draw_gallows(self): + gallow_size = (self._GALLOWX, self._GALLOWY) background = Image.new("RGBA", gallow_size, color=(0, 0, 0, 0)) - bottom_line = self.__bad_line(int(self.__GALLOWX * 0.75), True) - bottom_line_x = int(self.__GALLOWX * 0.125) - bottom_line_y = self.__GALLOWY-(self.__LINEWIDTH*4) + bottom_line = self._bad_line(int(self._GALLOWX * 0.75), True) + bottom_line_x = int(self._GALLOWX * 0.125) + bottom_line_y = self._GALLOWY-(self._LINEWIDTH*4) paste_position = (bottom_line_x, bottom_line_y) background.paste(bottom_line, paste_position, bottom_line) - line_two = self.__bad_line(self.__GALLOWY-self.__LINEWIDTH*6) - line_two_x = int(self.__GALLOWX*(0.75*self.__PHI)) - line_two_y = self.__LINEWIDTH*2 + line_two = self._bad_line(self._GALLOWY-self._LINEWIDTH*6) + line_two_x = int(self._GALLOWX*(0.75*self._PHI)) + line_two_y = self._LINEWIDTH*2 paste_position = (line_two_x, line_two_y) background.paste(line_two, paste_position, line_two) - top_line = self.__bad_line(int(self.__GALLOWY*0.30), True) - paste_x = int(self.__GALLOWX*(0.75*self.__PHI))-self.__LINEWIDTH - paste_position = (paste_x, self.__LINEWIDTH*3) + top_line = self._bad_line(int(self._GALLOWY*0.30), True) + paste_x = int(self._GALLOWX*(0.75*self._PHI))-self._LINEWIDTH + paste_position = (paste_x, self._LINEWIDTH*3) background.paste(top_line, paste_position, top_line) - last_line = self.__bad_line(int(self.__GALLOWY*0.125)) - paste_x += int(self.__GALLOWY*0.30) - background.paste(last_line, (paste_x, self.__LINEWIDTH*3), last_line) + last_line = self._bad_line(int(self._GALLOWY*0.125)) + paste_x += int(self._GALLOWY*0.30) + background.paste(last_line, (paste_x, self._LINEWIDTH*3), last_line) return background - def __draw_letter_lines(self, word: str, guessed: list, misses: int): - letter_width = self.__LETTERLINELENGTH+self.__LETTERLINEDISTANCE + def _draw_letter_lines(self, word: str, guessed: list, misses: int): + letter_width = self._LETTERLINELENGTH+self._LETTERLINEDISTANCE image_width = letter_width*len(word) - image_size = (image_width, self.__LETTERLINELENGTH+self.__LINEWIDTH*3) + image_size = (image_width, self._LETTERLINELENGTH+self._LINEWIDTH*3) letter_lines = Image.new("RGBA", image_size, color=(0, 0, 0, 0)) for i, letter in enumerate(word): - line = self.__bad_line(self.__LETTERLINELENGTH, True) - paste_x = i*(self.__LETTERLINELENGTH+self.__LETTERLINEDISTANCE) - paste_position = (paste_x, self.__LETTERLINELENGTH) + line = self._bad_line(self._LETTERLINELENGTH, True) + paste_x = i*(self._LETTERLINELENGTH+self._LETTERLINEDISTANCE) + paste_position = (paste_x, self._LETTERLINELENGTH) letter_lines.paste(line, paste_position, line) if guessed[i]: - letter_drawing = self.__bad_text(letter, True) - letter_width = self.__FONT.getsize(letter)[0] - letter_x = i*(self.__LETTERLINELENGTH+self.__LETTERLINEDISTANCE) + letter_drawing = self._bad_text(letter, True) + letter_width = self._FONT.getsize(letter)[0] + letter_x = i*(self._LETTERLINELENGTH+self._LETTERLINEDISTANCE) letter_x -= (letter_width//2) - letter_x += (self.__LETTERLINELENGTH//2)+(self.__LINEWIDTH*2) + letter_x += (self._LETTERLINELENGTH//2)+(self._LINEWIDTH*2) letter_lines.paste( letter_drawing, (letter_x, 0), letter_drawing ) elif misses == 6: - letter_drawing = self.__bad_text(letter, True, (242, 66, 54)) - letter_width = self.__FONT.getsize(letter)[0] - letter_x = i*(self.__LETTERLINELENGTH+self.__LETTERLINEDISTANCE) + letter_drawing = self._bad_text(letter, True, (242, 66, 54)) + letter_width = self._FONT.getsize(letter)[0] + letter_x = i*(self._LETTERLINELENGTH+self._LETTERLINEDISTANCE) letter_x -= (letter_width//2) - letter_x += (self.__LETTERLINELENGTH//2)+(self.__LINEWIDTH*2) + letter_x += (self._LETTERLINELENGTH//2)+(self._LINEWIDTH*2) letter_lines.paste( letter_drawing, (letter_x, 0), @@ -591,7 +591,7 @@ class DrawHangman(): return letter_lines - def __shortest_dist(self, positions: list, new_position: tuple): + def _shortest_dist(self, positions: list, new_position: tuple): shortest_dist = math.inf for i, j in positions: x_distance = abs(i-new_position[0]) @@ -601,18 +601,18 @@ class DrawHangman(): shortest_dist = dist return shortest_dist - def __draw_misses(self, guesses: list, word: str): + def _draw_misses(self, guesses: list, word: str): background = Image.new("RGBA", (600, 400), color=(0, 0, 0, 0)) pos = [] for guess in guesses: if guess not in word: placed = False while not placed: - letter = self.__bad_text(guess, True) - w, h = self.__FONT.getsize(guess) + letter = self._bad_text(guess, True) + w, h = self._FONT.getsize(guess) x = random.randint(0, 600-w) y = random.randint(0, 400-h) - if self.__shortest_dist(pos, (x, y)) > 70: + if self._shortest_dist(pos, (x, y)) > 70: pos.append((x, y)) background.paste(letter, (x, y), letter) placed = True @@ -628,27 +628,27 @@ class DrawHangman(): channel: str The id of the channel the game is in. """ - self.__bot.log("Drawing hangman image") + self._bot.log("Drawing hangman image") random.seed(game_id) background = Image.open("gwendolyn/resources/paper.jpg") - gallow = self.__draw_gallows() - man = self.__draw_man(misses, game_id) + gallow = self._draw_gallows() + man = self._draw_man(misses, game_id) random.seed(game_id) letter_line_parameters = [word, guessed, misses] - letter_lines = self.__draw_letter_lines(*letter_line_parameters) + letter_lines = self._draw_letter_lines(*letter_line_parameters) random.seed(game_id) - misses = self.__draw_misses(list(guessed_letters), word) + misses = self._draw_misses(list(guessed_letters), word) background.paste(gallow, (100, 100), gallow) background.paste(man, (300, 210), man) background.paste(letter_lines, (120, 840), letter_lines) background.paste(misses, (600, 150), misses) - misses_text = self.__bad_text("MISSES", False) + misses_text = self._bad_text("MISSES", False) misses_text_width = misses_text.size[0] background.paste(misses_text, (850-misses_text_width//2, 50), misses_text) diff --git a/gwendolyn/resources/games/default_images/hangman.jpg b/gwendolyn/resources/games/default_images/hangman.jpg deleted file mode 100644 index cafeefb..0000000 Binary files a/gwendolyn/resources/games/default_images/hangman.jpg and /dev/null differ diff --git a/gwendolyn/resources/games/default_images/hangman.png b/gwendolyn/resources/games/default_images/hangman.png new file mode 100644 index 0000000..3370360 Binary files /dev/null and b/gwendolyn/resources/games/default_images/hangman.png differ diff --git a/gwendolyn/resources/long_strings.json b/gwendolyn/resources/long_strings.json index c5964a3..c9c5e42 100644 --- a/gwendolyn/resources/long_strings.json +++ b/gwendolyn/resources/long_strings.json @@ -7,7 +7,7 @@ "Blackjack double": "Adding another {} GwendoBucks to {}'s bet and drawing another card.", "Blackjack different cards": "You can only split if your cards have the same value", "Blackjack split": "Splitting {}'s hand into 2. Adding their original bet to the second hand. You can use \"/blackjack hit/stand/double 1\" and \"/blackjack hit/stand/double 2\" to play the different hands.", - "Blackjack started": "Blackjack game started. Use \"/blackjack bet [amount]\" to enter the game within the next 30 seconds.", + "Blackjack started": "Blackjack game started. Use the buttons or \"/blackjack bet [amount]\" to enter the game within the next 30 seconds.", "Blackjack going on": "There's already a blackjack game going on. Try again in a few minutes.", "Stock value": "The current {} stock is valued at **{}** GwendoBucks", "Stock parameters": "You must give both a stock name and an amount of GwendoBucks you wish to spend.", diff --git a/gwendolyn/resources/slash_parameters.json b/gwendolyn/resources/slash_parameters.json index e5ecf1c..532d552 100644 --- a/gwendolyn/resources/slash_parameters.json +++ b/gwendolyn/resources/slash_parameters.json @@ -45,68 +45,16 @@ "name" : "cards", "description" : "Get a count of the cards used in blackjack games" }, - "blackjack_double" : { - "base" : "blackjack", - "name" : "double", - "description" : "Double your bet in blackjack", - "options" : [ - { - "name" : "hand", - "description" : "The number of the hand to double your bet on", - "type" : 4, - "required" : "false" - } - ] - }, "blackjack_hilo" : { "base" : "blackjack", "name" : "hilo", "description" : "Get the current hi-lo value for the cards used in blackjack games" }, - "blackjack_hit" : { - "base" : "blackjack", - "name" : "hit", - "description" : "Hit on your hand in blackjack", - "options" : [ - { - "name" : "hand", - "description" : "The number of the hand to hit if you've split", - "type" : 4, - "required" : "false" - } - ] - }, "blackjack_shuffle" : { "base" : "blackjack", "name" : "shuffle", "description" : "Shuffle the cards used in blackjack games" }, - "blackjack_split" : { - "base" : "blackjack", - "name" : "split", - "description" : "Split your hand in blackjack", - "options" : [ - { - "name" : "hand", - "description" : "The number of the hand to split, in case you've already split once", - "type" : 4, - "required" : "false" - } - ] - }, - "blackjack_stand" : { - "base" : "blackjack", - "name" : "stand", - "description" : "Stand on your hand in blackjack", - "options" : [ - { - "name" : "hand", - "description" : "The number of the hand to stand if you've split", - "type" : 4, - "required" : "false" - } - ] - }, "blackjack_start" : { "base" : "blackjack", "name" : "start", diff --git a/gwendolyn/utils/event_handlers.py b/gwendolyn/utils/event_handlers.py index 8fa864c..e657681 100644 --- a/gwendolyn/utils/event_handlers.py +++ b/gwendolyn/utils/event_handlers.py @@ -15,6 +15,7 @@ from discord.ext import commands # Used to compare errors with command from discord_slash.context import SlashContext, ComponentContext from gwendolyn.utils.util_functions import decode_id +from gwendolyn.exceptions import InvalidInteraction class EventHandler(): @@ -64,7 +65,8 @@ class EventHandler(): async def on_component(self, ctx: ComponentContext): info = decode_id(ctx.custom_id) self.bot.log(f"Component action with info {info}") - channel = ctx.origin_message.channel + channel = ctx.channel + author = str(ctx.author_id) if info[0].lower() == "plex": if info[1].lower() == "movie": @@ -73,25 +75,29 @@ class EventHandler(): info[2], not isinstance(channel, discord.DMChannel) ) + return - elif info[1].lower() == "show": + if info[1].lower() == "show": await self.bot.other.plex.add_show( ctx.origin_message, info[2], not isinstance(channel, discord.DMChannel) ) + return - elif info[0].lower() == "hangman": - if str(ctx.author_id) == info[2]: - if info[1].lower() == "guess": - await self.bot.games.hangman.guess(ctx, *info[3:]) - elif info[1].lower() == "end": - await self.bot.games.hangman.stop(ctx, *info[3:]) + elif info[0].lower() == "hangman" and author == info[2]: + if info[1].lower() == "guess": + await self.bot.games.hangman.guess(ctx, *info[3:]) + return + + if info[1].lower() == "end": + await self.bot.games.hangman.stop(ctx, *info[3:]) + return elif info[0].lower() == "connectfour": connect_four = self.bot.games.connect_four - if info[1].lower() == "place" and str(ctx.author_id) == info[2]: - params = [ + if info[1].lower() == "place" and author == info[2]: + await connect_four.place_piece( ctx, info[3], int(info[4]), @@ -99,14 +105,20 @@ class EventHandler(): int(info[7]), ctx.author_id, int(info[8]) - ] - await connect_four.place_piece(*params) - if info[1].lower() == "end": - if str(ctx.author_id) in [info[2], info[3]]: - params = [ - ctx, [int(info[2]), int(info[3])], info[4], info[5] - ] - await connect_four.surrender(*params) + ) + return + + if info[1].lower() == "end" and author in [info[2], info[3]]: + await connect_four.surrender( + ctx, [int(info[2]), int(info[3])], info[4], info[5] + ) + return + + elif info[0].lower() == "blackjack": + await self.bot.games.blackjack.decode_interaction(ctx, info[1:]) + return + + raise InvalidInteraction(ctx.custom_id, info)