"""Base class for the games.""" import random from typing import Union 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, game_name: int, drawer): """Initialize the class.""" self.bot = bot self.long_strings = self.bot.long_strings self.resources = "gwendolyn/resources/games/" self.game_name = game_name self.draw = drawer(bot, self) 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}"] 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"): 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, buttons: list[tuple[str, list]] = None, delete=True): old_image = await 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, buttons: list[tuple[str, list]] = None, delete = True): new_game['_id'] = str(channel.id) self._insert_document(new_game) await self._send_image(channel, buttons, delete) 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(DatabaseGame): """The class for card games.""" def __init__(self, bot, game_name, drawer, deck_used): """Initialize the class.""" 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}") 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 BoardGame(GameBase): async 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 opponent, (difficulty, difficulty_text) 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.default_size = None self.default_color = None self.images_path = f"{self.resources}images/{game_name}" def _get_size(self, game: dict): return self.default_size def _draw_image(self, game: dict, image: Image.Image): pass def draw(self, channel: str): game = self.game.access_document(channel) if self.default_image is not None: image = Image.open(self.default_image) else: image = Image.new("RGB", self._get_size(game), self.default_color) 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