diff --git a/werewolf/game.py b/werewolf/game.py index 79d8455..ee84b09 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -19,6 +19,14 @@ log = logging.getLogger("red.fox_v3.werewolf.game") HALF_DAY_LENGTH = 24 # FixMe: to 120 later for 4 minute days +async def anyone_has_role( + member_list: List[discord.Member], role: discord.Role +) -> Union[None, discord.Member]: + return await AsyncIter(member_list).find( + lambda m: AsyncIter(m.roles).find(lambda r: r.id == role.id) + ) + + class Game: """ Base class to run a single game of Werewolf @@ -129,6 +137,7 @@ class Game: self.roles = [] return False + # If there's no game role, make the role and delete it later in `self.to_delete` if self.game_role is None: try: self.game_role = await ctx.guild.create_role( @@ -144,14 +153,25 @@ class Game: ) self.roles = [] return False - try: - for player in self.players: - await player.member.add_roles(*[self.game_role]) - except discord.Forbidden: - await ctx.send( - f"Unable to add role **{self.game_role.name}**\nBot is missing `manage_roles` permissions" - ) - return False + + anyone_with_role = await anyone_has_role(self.guild.members, self.game_role) + if anyone_with_role is not None: + await ctx.maybe_send_embed( + f"{anyone_with_role.display_name} has the game role, " + f"can't continue until no one has the role" + ) + return False + + try: + for player in self.players: + await player.member.add_roles(*[self.game_role]) + except discord.Forbidden: + log.exception(f"Unable to add role **{self.game_role.name}**") + await ctx.send( + f"Unable to add role **{self.game_role.name}**\n" + f"Bot is missing `manage_roles` permissions" + ) + return False await self.assign_roles() @@ -223,9 +243,10 @@ class Game: self.started = True # Assuming everything worked so far log.debug("Pre at_game_start") - await self._at_game_start() # This will queue channels and votegroups to be made + await self._at_game_start() # This will add votegroups to self.p_channels log.debug("Post at_game_start") - for channel_id in self.p_channels: + log.debug(f"Private channels: {self.p_channels}") + for channel_id in self.p_channels.keys(): log.debug("Setup Channel id: " + channel_id) overwrite = { self.guild.default_role: discord.PermissionOverwrite(read_messages=False), @@ -251,6 +272,8 @@ class Game: self.p_channels[channel_id]["channel"] = channel + self.to_delete.add(channel) + if self.p_channels[channel_id]["votegroup"] is not None: vote_group = self.p_channels[channel_id]["votegroup"](self, channel) @@ -259,8 +282,10 @@ class Game: self.vote_groups[channel_id] = vote_group log.debug("Pre-cycle") - await asyncio.sleep(1) - await asyncio.ensure_future(self._cycle()) # Start the loop + await asyncio.sleep(0) + + asyncio.create_task(self._cycle()) # Start the loop + return True # ###########START Notify structure############ async def _cycle(self): @@ -553,13 +578,14 @@ class Game: try: await asyncio.sleep(1) # This will have multiple calls self.p_channels[channel_id]["players"].append(role.player) - if votegroup is not None: - self.p_channels[channel_id]["votegroup"] = votegroup except AttributeError: continue else: break + if votegroup is not None: + self.p_channels[channel_id]["votegroup"] = votegroup + async def join(self, member: discord.Member, channel: discord.TextChannel): """ Have a member join a game @@ -574,14 +600,15 @@ class Game: self.players.append(Player(member)) - if self.game_role is not None: - try: - await member.add_roles(*[self.game_role]) - except discord.Forbidden: - await channel.send( - f"Unable to add role **{self.game_role.name}**\n" - f"Bot is missing `manage_roles` permissions" - ) + # Add the role during setup, not before + # if self.game_role is not None: + # try: + # await member.add_roles(*[self.game_role]) + # except discord.Forbidden: + # await channel.send( + # f"Unable to add role **{self.game_role.name}**\n" + # f"Bot is missing `manage_roles` permissions" + # ) await channel.send( f"{member.display_name} has been added to the game, " @@ -908,7 +935,7 @@ class Game: # Remove game_role access for potential archiving for now reason = "(BOT) End of WW game" for obj in self.to_delete: - log.debug(f"End_game: Deleting object {obj}") + log.debug(f"End_game: Deleting object {obj.__repr__()}") await obj.delete(reason=reason) try: @@ -926,6 +953,17 @@ class Game: except (discord.HTTPException, discord.NotFound, discord.errors.NotFound): pass + for player in self.players: + try: + await player.member.remove_roles(*[self.game_role]) + except discord.Forbidden: + log.exception(f"Unable to add remove **{self.game_role.name}**") + # await ctx.send( + # f"Unable to add role **{self.game_role.name}**\n" + # f"Bot is missing `manage_roles` permissions" + # ) + pass + # Optional dynamic channels/categories def add_ww_listener(self, func, priority=0, name=None): diff --git a/werewolf/player.py b/werewolf/player.py index 7f10758..48885a8 100644 --- a/werewolf/player.py +++ b/werewolf/player.py @@ -20,6 +20,9 @@ class Player: self.muted = False self.protected = False + def __repr__(self): + return f"{self.__class__.__name__}({self.member})" + async def assign_role(self, role): """ Give this player a role diff --git a/werewolf/role.py b/werewolf/role.py index db7b852..0997b56 100644 --- a/werewolf/role.py +++ b/werewolf/role.py @@ -73,7 +73,7 @@ class Role(WolfListener): self.properties = {} # Extra data for other roles (i.e. arsonist) def __repr__(self): - return self.__class__.__name__ + return f"{self.__class__.__name__}({self.player.__repr__()})" async def assign_player(self, player): """ @@ -84,6 +84,8 @@ class Role(WolfListener): player.role = self self.player = player + log.debug(f"Assigned {self} to {player}") + async def get_alignment(self, source=None): """ Interaction for powerful access of alignment diff --git a/werewolf/roles/seer.py b/werewolf/roles/seer.py index f6bd857..983fd14 100644 --- a/werewolf/roles/seer.py +++ b/werewolf/roles/seer.py @@ -15,6 +15,7 @@ log = logging.getLogger("red.fox_v3.werewolf.role.seer") class Seer(Role): rand_choice = True + town_balance = 4 category = [ CATEGORY_TOWN_RANDOM, CATEGORY_TOWN_INVESTIGATIVE, diff --git a/werewolf/roles/vanillawerewolf.py b/werewolf/roles/vanillawerewolf.py index 74e8d96..8abdea2 100644 --- a/werewolf/roles/vanillawerewolf.py +++ b/werewolf/roles/vanillawerewolf.py @@ -22,21 +22,6 @@ class VanillaWerewolf(Role): "Vote to kill players at night with `[p]ww vote `" ) - def __init__(self, game): - super().__init__(game) - - # self.action_list = [ - # (self._at_game_start, 1), # (Action, Priority) - # (self._at_day_start, 0), - # (self._at_voted, 0), - # (self._at_kill, 0), - # (self._at_hang, 0), - # (self._at_day_end, 0), - # (self._at_night_start, 0), - # (self._at_night_end, 0), - # (self._at_visit, 0) - # ] - async def see_alignment(self, source=None): """ Interaction for investigative roles attempting diff --git a/werewolf/roles/villager.py b/werewolf/roles/villager.py index d669ef9..eb0b2c9 100644 --- a/werewolf/roles/villager.py +++ b/werewolf/roles/villager.py @@ -7,12 +7,9 @@ log = logging.getLogger("red.fox_v3.werewolf.role.villager") class Villager(Role): - # Determines if it can be picked as a random role (False for unusually disruptive roles) rand_choice = True - town_balance = 1 - category = [CATEGORY_TOWN_RANDOM] # List of enrolled categories (listed above) alignment = ALIGNMENT_TOWN # 1: Town, 2: Werewolf, 3: Neutral channel_id = "" # Empty for no private channel @@ -23,9 +20,6 @@ class Villager(Role): "Lynch players during the day with `[p]ww vote `" ) - def __init__(self, game): - super().__init__(game) - async def see_alignment(self, source=None): """ Interaction for investigative roles attempting diff --git a/werewolf/votegroup.py b/werewolf/votegroup.py index 2f0b3a0..d8411fb 100644 --- a/werewolf/votegroup.py +++ b/werewolf/votegroup.py @@ -75,7 +75,6 @@ class VoteGroup(WolfListener): if not self.players: # TODO: Confirm deletion - self.game.to_delete.add(self) pass async def vote(self, target, author, target_id): diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 742a890..599796c 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -1,9 +1,11 @@ import logging +from typing import List, Union import discord from redbot.core import Config, checks, commands from redbot.core.bot import Red from redbot.core.commands import Cog +from redbot.core.utils import AsyncIter from redbot.core.utils.menus import DEFAULT_CONTROLS, menu from werewolf.builder import ( @@ -18,6 +20,14 @@ from werewolf.game import Game log = logging.getLogger("red.fox_v3.werewolf") +async def anyone_has_role( + member_list: List[discord.Member], role: discord.Role +) -> Union[None, discord.Member]: + return await AsyncIter(member_list).find( + lambda m: AsyncIter(m.roles).find(lambda r: r.id == role.id) + ) + + class Werewolf(Cog): """ Base to host werewolf on a guild @@ -189,12 +199,15 @@ class Werewolf(Cog): return await game.join(ctx.author, ctx.channel) + await ctx.tick() @commands.guild_only() @ww.command(name="code") async def ww_code(self, ctx: commands.Context, code): """ - Adjust game code + Adjusts the game code. + + See `[p]buildgame` to generate a new code """ game = await self._get_game(ctx) @@ -204,6 +217,7 @@ class Werewolf(Cog): return await game.set_code(ctx, code) + await ctx.tick() @commands.guild_only() @ww.command(name="quit") @@ -215,6 +229,7 @@ class Werewolf(Cog): game = await self._get_game(ctx) await game.quit(ctx.author, ctx.channel) + await ctx.tick() @commands.guild_only() @ww.command(name="start") @@ -229,6 +244,8 @@ class Werewolf(Cog): if not await game.setup(ctx): pass # ToDo something? + await ctx.tick() + @commands.guild_only() @ww.command(name="stop") async def ww_stop(self, ctx: commands.Context): @@ -245,6 +262,7 @@ class Werewolf(Cog): game = await self._get_game(ctx) game.game_over = True + await game.current_action.cancel() await ctx.send("Game has been stopped") @commands.guild_only() @@ -358,7 +376,7 @@ class Werewolf(Cog): else: await ctx.send("Role ID not found") - async def _get_game(self, ctx: commands.Context, game_code=None): + async def _get_game(self, ctx: commands.Context, game_code=None) -> Union[Game, None]: guild: discord.Guild = getattr(ctx, "guild", None) if guild is None: