310 lines
11 KiB
Python
310 lines
11 KiB
Python
"""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):
|
|
self._insert_document(new_game)
|
|
await self._send_image(channel, buttons)
|
|
|
|
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 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.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
|