Files
Gwendolyn/gwendolyn_old/funcs/games/game_base.py
2024-10-28 13:05:06 +01:00

319 lines
12 KiB
Python

"""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