"""Base class for the games.""" import random from typing import Union from discord import File, User from discord.abc import Messageable from interactions import Button, ActionRow from interactions import InteractionContext as IntCont from PIL import ImageFont, Image, ImageDraw from gwendolyn_old.exceptions import GameNotInDatabase from gwendolyn_old.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(Button( style=style, label=label, custom_id=custom_id )) action_rows = [] for i in range(((len(button_objects)-1)//5)+1): action_rows.append(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