import logging from typing import Dict, Callable, List, Union from functools import wraps from collections import defaultdict import discord logger = logging.getLogger(__name__) def started_only(func): """ Decorator for *methods* of Game that need the game to be started. """ @wraps(func) def decorated(obj: 'Game', *args, **kwargs): if not obj.is_started(): raise RuntimeError("This function only works on running games!") return func(obj, *args, **kwargs) return decorated def vote_running(func): """ Decorator for *methods* of Game that need a vote to be running """ @wraps(func) def decorated(obj: 'Game', *args, **kwargs): if not obj.is_vote_running(): raise RuntimeError("This function only works if a vote is running!") return func(obj, *args, **kwargs) return decorated class Game: """ Game state for one guild """ def __init__(self, config_dict: Dict, save_function: Callable, guild: discord.Guild): self.config = config_dict self.save_function = save_function self.guild = guild @staticmethod def new_dict(): return { "game_started": False, } def is_started(self): return self.config["game_started"] @started_only def get_players_id(self) -> List[int]: return self.config["players"] @started_only def get_players(self) -> List[discord.Member]: return [self.guild.get_member(player) for player in self.get_players_id()] @started_only def get_player_info(self, player: Union[int, discord.Member]): if isinstance(player, discord.Member): player = player.id return self.config["player_info"][str(player)] @started_only def get_gm_role_id(self) -> int: return self.config["gm_role"] @started_only def get_gm_role(self) -> discord.Role: return self.guild.get_role(self.get_gm_role_id()) @started_only def is_gm(self, user: discord.Member) -> bool: return self.get_gm_role() in user.roles @started_only def get_game_category_id(self) -> int: return self.config["category"] @started_only def get_game_category(self) -> discord.CategoryChannel: return self.guild.get_channel(self.get_game_category_id()) @started_only def get_votes_channel_id(self) -> int: return self.config["votes_chan"] @started_only def get_votes_channel(self) -> discord.TextChannel: return self.guild.get_channel(self.get_votes_channel_id()) @started_only def is_vote_running(self) -> bool: return self.config["vote"] is not None @vote_running def is_vote_passing(self) -> bool: vote_count = defaultdict(int) for player_id in self.get_players_id(): vote_count[self.config["vote"][str(player_id)]] += 1 return vote_count[True] > vote_count[False] async def start(self, gm_role: discord.Role, player_role: discord.Role): if self.is_started(): raise ValueError("Game already started") logger.info(f"[{self.guild.name}] Starting game") self.config["gm_role"] = gm_role.id self.config["player_role"] = player_role.id self.config["players"] = [member.id for member in player_role.members] self.config["player_info"] = {str(player): {} for player in self.config["players"]} self.config["vote"] = None permissions = { self.guild.default_role: discord.PermissionOverwrite(send_messages = False), gm_role: discord.PermissionOverwrite(send_messages = True), player_role: discord.PermissionOverwrite(send_messages = True), } game_category = await self.guild.create_category("In-game", overwrites = permissions) self.config["category"] = game_category.id logger.debug(f"[{self.guild.name}] Created game category") permissions = { self.guild.default_role: discord.PermissionOverwrite(read_messages = False), gm_role: discord.PermissionOverwrite(read_messages = True), } self.config["admin_chan"] = (await game_category.create_text_channel("admin", overwrites = permissions)).id logger.debug(f"[{self.guild.name}] Created admin channel") permissions = { self.guild.default_role: discord.PermissionOverwrite(send_messages = False), gm_role: discord.PermissionOverwrite(send_messages = True), } self.config["announce_chan"] = (await game_category.create_text_channel("announce", overwrites = permissions)).id self.config["votes_chan"] = (await game_category.create_text_channel("votes", overwrites = permissions)).id logger.debug(f"[{self.guild.name}] Created announcements and votes channels") self.config["discussion_chan"] = (await game_category.create_text_channel("discussion")).id # Permissions are inherited from the category logger.debug(f"[{self.guild.name}] Created discussion channel") for player in player_role.members: channel_permissions = { self.guild.default_role: discord.PermissionOverwrite(read_messages = False), player: discord.PermissionOverwrite(read_messages = True), gm_role: discord.PermissionOverwrite(read_messages = True), } player_channel = await game_category.create_text_channel(player.name, overwrites = channel_permissions) self.config["player_info"][str(player.id)]["channel"] = player_channel.id logger.debug(f"[{self.guild.name}] Created player channels") self.config["game_started"] = True self.save_function() @started_only async def delete(self): category = self.get_game_category() for channel in category.channels: await channel.delete() await category.delete() self.config.clear() self.config.update(self.new_dict()) self.save_function() async def start_vote(self, president: discord.Member, chancellor: discord.Member): if self.is_vote_running(): raise RuntimeError("A vote is already running") logging.debug(f"[{self.guild.name}] Starting vote") self.config["vote"] = { "president": president.id, "chancellor": chancellor.id, "message": None, "revealed": False, } self.config["vote"].update({str(player_id): None for player_id in self.get_players_id()}) self.save_function() await self.update_vote_message() @vote_running async def update_vote_message(self): logging.debug(f"[{self.guild.name}] Updating vote message") president = self.config["vote"]["president"] chancellor = self.config["vote"]["chancellor"] message_content = [ "**Citizens are called to vote**", "Do you want to elect the following government?", f":crown: <@{president}> as president", f":person_in_tuxedo: <@{chancellor}> as chancellor", "", ] for player_id in self.get_players_id(): player_vote = self.config["vote"][str(player_id)] if player_vote is None: message_content.append(f":black_large_square: <@{player_id}> has not voted") else: if self.config["vote"]["revealed"]: if player_vote: message_content.append(f":green_square: <@{player_id}> has voted JA") else: message_content.append(f":red_square: <@{player_id}> has voted NEIN") else: # Player has voted but the vote should not be revealed message_content.append(f":white_large_square: <@{player_id}> has voted") message_content_str = "\n".join(message_content) if self.config["vote"]["message"] is None: self.config["vote"]["message"] = (await self.get_votes_channel().send(message_content_str, allowed_mentions = discord.AllowedMentions.none())).id else: await (await self.get_votes_channel().fetch_message(self.config["vote"]["message"])).edit(content = message_content_str, allowed_mentions = discord.AllowedMentions.none())