From 0d59a5220ff911990c831ed2a65c503f184c8932 Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 5 Oct 2018 13:06:17 -0400 Subject: [PATCH 01/43] ctx.me instead of ctx.guild.me to work in dm's --- werewolf/builder.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/werewolf/builder.py b/werewolf/builder.py index da0bf1e..4364cb1 100644 --- a/werewolf/builder.py +++ b/werewolf/builder.py @@ -190,7 +190,7 @@ async def encode(roles, rand_roles): async def next_group(ctx: commands.Context, pages: list, controls: dict, message: discord.Message, page: int, timeout: float, emoji: str): - perms = message.channel.permissions_for(ctx.guild.me) + perms = message.channel.permissions_for(ctx.me) if perms.manage_messages: # Can manage messages, so remove react try: await message.remove_reaction(emoji, ctx.author) @@ -210,7 +210,7 @@ async def next_group(ctx: commands.Context, pages: list, async def prev_group(ctx: commands.Context, pages: list, controls: dict, message: discord.Message, page: int, timeout: float, emoji: str): - perms = message.channel.permissions_for(ctx.guild.me) + perms = message.channel.permissions_for(ctx.me) if perms.manage_messages: # Can manage messages, so remove react try: await message.remove_reaction(emoji, ctx.author) @@ -295,7 +295,7 @@ class GameBuilder: async def list_roles(self, ctx: commands.Context, pages: list, controls: dict, message: discord.Message, page: int, timeout: float, emoji: str): - perms = message.channel.permissions_for(ctx.guild.me) + perms = message.channel.permissions_for(ctx.me) if perms.manage_messages: # Can manage messages, so remove react try: await message.remove_reaction(emoji, ctx.author) @@ -310,7 +310,7 @@ class GameBuilder: async def select_page(self, ctx: commands.Context, pages: list, controls: dict, message: discord.Message, page: int, timeout: float, emoji: str): - perms = message.channel.permissions_for(ctx.guild.me) + perms = message.channel.permissions_for(ctx.me) if perms.manage_messages: # Can manage messages, so remove react try: await message.remove_reaction(emoji, ctx.author) From 0ee0199b11f852bbdc13e33cbacf66aafd184576 Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 5 Oct 2018 13:50:55 -0400 Subject: [PATCH 02/43] role `__init__.py` experiment --- werewolf/builder.py | 4 +--- werewolf/roles/__init__.py | 5 +++++ 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 werewolf/roles/__init__.py diff --git a/werewolf/builder.py b/werewolf/builder.py index 4364cb1..ce00b28 100644 --- a/werewolf/builder.py +++ b/werewolf/builder.py @@ -8,9 +8,7 @@ import discord # Import all roles here from redbot.core import commands -from werewolf.roles.seer import Seer -from werewolf.roles.vanillawerewolf import VanillaWerewolf -from werewolf.roles.villager import Villager +from werewolf.roles import Seer, VanillaWerewolf, Villager from redbot.core.utils.menus import menu, prev_page, next_page, close_menu # All roles in this list for iterating diff --git a/werewolf/roles/__init__.py b/werewolf/roles/__init__.py new file mode 100644 index 0000000..ba929e5 --- /dev/null +++ b/werewolf/roles/__init__.py @@ -0,0 +1,5 @@ +from .seer import Seer +from .shifter import Shifter +from .vanillawerewolf import VanillaWerewolf +from .villager import Villager + From 849262969c2c1936a45a0ec5c3cd73d9740dc9aa Mon Sep 17 00:00:00 2001 From: bobloy Date: Wed, 5 Dec 2018 14:59:10 -0500 Subject: [PATCH 03/43] forgot some await's --- werewolf/game.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index 181c198..78bd6f6 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -210,7 +210,7 @@ class Game: print("Pre-cycle") await asyncio.sleep(1) - asyncio.ensure_future(self._cycle()) # Start the loop + await asyncio.ensure_future(self._cycle()) # Start the loop ############START Notify structure############ async def _cycle(self): @@ -276,7 +276,7 @@ class Game: # Need a loop here to wait for trial to end (can_vote?) while self.ongoing_vote: - asyncio.sleep(5) + await asyncio.sleep(5) if check(): return From 339492d6d923f421b187cc33d0d65837bc022b1f Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 18 Sep 2020 14:10:04 -0400 Subject: [PATCH 04/43] Black formatting --- werewolf/builder.py | 153 ++++++++++++++++++-------- werewolf/game.py | 250 ++++++++++++++++++++++++++++-------------- werewolf/role.py | 18 +-- werewolf/votegroup.py | 2 +- 4 files changed, 281 insertions(+), 142 deletions(-) diff --git a/werewolf/builder.py b/werewolf/builder.py index b6a52df..1648b45 100644 --- a/werewolf/builder.py +++ b/werewolf/builder.py @@ -17,7 +17,7 @@ from redbot.core.utils.menus import menu, prev_page, next_page, close_menu ROLE_LIST = sorted([Villager, Seer, VanillaWerewolf], key=lambda x: x.alignment) -ALIGNMENT_COLORS = [0x008000, 0xff0000, 0xc0c0c0] +ALIGNMENT_COLORS = [0x008000, 0xFF0000, 0xC0C0C0] TOWN_ROLES = [(idx, role) for idx, role in enumerate(ROLE_LIST) if role.alignment == 1] WW_ROLES = [(idx, role) for idx, role in enumerate(ROLE_LIST) if role.alignment == 2] OTHER_ROLES = [(idx, role) for idx, role in enumerate(ROLE_LIST) if role.alignment not in [0, 1]] @@ -26,21 +26,38 @@ ROLE_PAGES = [] PAGE_GROUPS = [0] ROLE_CATEGORIES = { - 1: "Random", 2: "Investigative", 3: "Protective", 4: "Government", - 5: "Killing", 6: "Power (Special night action)", - 11: "Random", 12: "Deception", 15: "Killing", 16: "Support", - 21: "Benign", 22: "Evil", 23: "Killing"} + 1: "Random", + 2: "Investigative", + 3: "Protective", + 4: "Government", + 5: "Killing", + 6: "Power (Special night action)", + 11: "Random", + 12: "Deception", + 15: "Killing", + 16: "Support", + 21: "Benign", + 22: "Evil", + 23: "Killing", +} CATEGORY_COUNT = [] def role_embed(idx, role, color): - embed = discord.Embed(title="**{}** - {}".format(idx, str(role.__name__)), description=role.game_start_message, - color=color) - embed.add_field(name='Alignment', value=['Town', 'Werewolf', 'Neutral'][role.alignment - 1], inline=True) - embed.add_field(name='Multiples Allowed', value=str(not role.unique), inline=True) - embed.add_field(name='Role Type', value=", ".join(ROLE_CATEGORIES[x] for x in role.category), inline=True) - embed.add_field(name='Random Option', value=str(role.rand_choice), inline=True) + embed = discord.Embed( + title="**{}** - {}".format(idx, str(role.__name__)), + description=role.game_start_message, + color=color, + ) + embed.add_field( + name="Alignment", value=["Town", "Werewolf", "Neutral"][role.alignment - 1], inline=True + ) + embed.add_field(name="Multiples Allowed", value=str(not role.unique), inline=True) + embed.add_field( + name="Role Type", value=", ".join(ROLE_CATEGORIES[x] for x in role.category), inline=True + ) + embed.add_field(name="Random Option", value=str(role.rand_choice), inline=True) return embed @@ -60,7 +77,11 @@ def setup(): PAGE_GROUPS.append(len(ROLE_PAGES) - 1) for k, v in ROLE_CATEGORIES.items(): if 0 < k <= 6: - ROLE_PAGES.append(discord.Embed(title="RANDOM:Town Role", description="Town {}".format(v), color=0x008000)) + ROLE_PAGES.append( + discord.Embed( + title="RANDOM:Town Role", description="Town {}".format(v), color=0x008000 + ) + ) CATEGORY_COUNT.append(k) # Random WW Roles @@ -69,7 +90,12 @@ def setup(): for k, v in ROLE_CATEGORIES.items(): if 10 < k <= 16: ROLE_PAGES.append( - discord.Embed(title="RANDOM:Werewolf Role", description="Werewolf {}".format(v), color=0xff0000)) + discord.Embed( + title="RANDOM:Werewolf Role", + description="Werewolf {}".format(v), + color=0xFF0000, + ) + ) CATEGORY_COUNT.append(k) # Random Neutral Roles if len(ROLE_PAGES) - 1 not in PAGE_GROUPS: @@ -77,7 +103,10 @@ def setup(): for k, v in ROLE_CATEGORIES.items(): if 20 < k <= 26: ROLE_PAGES.append( - discord.Embed(title="RANDOM:Neutral Role", description="Neutral {}".format(v), color=0xc0c0c0)) + discord.Embed( + title="RANDOM:Neutral Role", description="Neutral {}".format(v), color=0xC0C0C0 + ) + ) CATEGORY_COUNT.append(k) @@ -187,9 +216,15 @@ async def encode(roles, rand_roles): return out_code -async def next_group(ctx: commands.Context, pages: list, - controls: dict, message: discord.Message, page: int, - timeout: float, emoji: str): +async def next_group( + ctx: commands.Context, + pages: list, + controls: dict, + message: discord.Message, + page: int, + timeout: float, + emoji: str, +): perms = message.channel.permissions_for(ctx.me) if perms.manage_messages: # Can manage messages, so remove react try: @@ -203,13 +238,18 @@ async def next_group(ctx: commands.Context, pages: list, else: page = PAGE_GROUPS[page] - return await menu(ctx, pages, controls, message=message, - page=page, timeout=timeout) + return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout) -async def prev_group(ctx: commands.Context, pages: list, - controls: dict, message: discord.Message, page: int, - timeout: float, emoji: str): +async def prev_group( + ctx: commands.Context, + pages: list, + controls: dict, + message: discord.Message, + page: int, + timeout: float, + emoji: str, +): perms = message.channel.permissions_for(ctx.me) if perms.manage_messages: # Can manage messages, so remove react try: @@ -218,18 +258,23 @@ async def prev_group(ctx: commands.Context, pages: list, pass page = PAGE_GROUPS[bisect.bisect_left(PAGE_GROUPS, page) - 1] - return await menu(ctx, pages, controls, message=message, - page=page, timeout=timeout) + return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout) def role_from_alignment(alignment): - return [role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]) - for idx, role in enumerate(ROLE_LIST) if alignment == role.alignment] + return [ + role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]) + for idx, role in enumerate(ROLE_LIST) + if alignment == role.alignment + ] def role_from_category(category): - return [role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]) - for idx, role in enumerate(ROLE_LIST) if category in role.category] + return [ + role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]) + for idx, role in enumerate(ROLE_LIST) + if category in role.category + ] def role_from_id(idx): @@ -242,8 +287,11 @@ def role_from_id(idx): def role_from_name(name: str): - return [role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]) - for idx, role in enumerate(ROLE_LIST) if name in role.__name__] + return [ + role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]) + for idx, role in enumerate(ROLE_LIST) + if name in role.__name__ + ] def say_role_list(code_list, rand_roles): @@ -268,7 +316,6 @@ def say_role_list(code_list, rand_roles): class GameBuilder: - def __init__(self): self.code = [] self.rand_roles = [] @@ -276,13 +323,13 @@ class GameBuilder: async def build_game(self, ctx: commands.Context): new_controls = { - '⏪': prev_group, + "⏪": prev_group, "⬅": prev_page, - '☑': self.select_page, + "☑": self.select_page, "➡": next_page, - '⏩': next_group, - '📇': self.list_roles, - "❌": close_menu + "⏩": next_group, + "📇": self.list_roles, + "❌": close_menu, } await ctx.send("Browse through roles and add the ones you want using the check mark") @@ -292,9 +339,16 @@ class GameBuilder: out = await encode(self.code, self.rand_roles) return out - async def list_roles(self, ctx: commands.Context, pages: list, - controls: dict, message: discord.Message, page: int, - timeout: float, emoji: str): + async def list_roles( + self, + ctx: commands.Context, + pages: list, + controls: dict, + message: discord.Message, + page: int, + timeout: float, + emoji: str, + ): perms = message.channel.permissions_for(ctx.me) if perms.manage_messages: # Can manage messages, so remove react try: @@ -304,12 +358,18 @@ class GameBuilder: await ctx.send(embed=say_role_list(self.code, self.rand_roles)) - return await menu(ctx, pages, controls, message=message, - page=page, timeout=timeout) - - async def select_page(self, ctx: commands.Context, pages: list, - controls: dict, message: discord.Message, page: int, - timeout: float, emoji: str): + return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout) + + async def select_page( + self, + ctx: commands.Context, + pages: list, + controls: dict, + message: discord.Message, + page: int, + timeout: float, + emoji: str, + ): perms = message.channel.permissions_for(ctx.me) if perms.manage_messages: # Can manage messages, so remove react try: @@ -322,5 +382,4 @@ class GameBuilder: else: self.code.append(page) - return await menu(ctx, pages, controls, message=message, - page=page, timeout=timeout) + return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout) diff --git a/werewolf/game.py b/werewolf/game.py index a31518f..95807a1 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -15,6 +15,7 @@ class Game: """ Base class to run a single game of Werewolf """ + vote_groups: Dict[str, VoteGroup] roles: List[Role] players: List[Player] @@ -22,19 +23,25 @@ class Game: default_secret_channel = { "channel": None, "players": [], - "votegroup": None # uninitialized VoteGroup + "votegroup": None, # uninitialized VoteGroup } morning_messages = [ "**The sun rises on day {} in the village..**", - "**Morning has arrived on day {}..**" + "**Morning has arrived on day {}..**", ] day_vote_count = 3 - def __init__(self, guild: discord.Guild, role: discord.Role = None, - category: discord.CategoryChannel = None, village: discord.TextChannel = None, - log_channel: discord.TextChannel = None, game_code=None): + def __init__( + self, + guild: discord.Guild, + role: discord.Role = None, + category: discord.CategoryChannel = None, + village: discord.TextChannel = None, + log_channel: discord.TextChannel = None, + game_code=None, + ): self.guild = guild self.game_code = game_code @@ -97,22 +104,28 @@ class Game: await self.get_roles(ctx) if len(self.players) != len(self.roles): - await ctx.maybe_send_embed("Player count does not match role count, cannot start\n" - "Currently **{} / {}**\n" - "Use `{}ww code` to pick a new game" - "".format(len(self.players), len(self.roles), ctx.prefix)) + await ctx.maybe_send_embed( + "Player count does not match role count, cannot start\n" + "Currently **{} / {}**\n" + "Use `{}ww code` to pick a new game" + "".format(len(self.players), len(self.roles), ctx.prefix) + ) self.roles = [] return False if self.game_role is None: try: - self.game_role = await ctx.guild.create_role(name="WW Players", - hoist=True, - mentionable=True, - reason="(BOT) Werewolf game role") + self.game_role = await ctx.guild.create_role( + name="WW Players", + hoist=True, + mentionable=True, + reason="(BOT) Werewolf game role", + ) self.to_delete.add(self.game_role) except (discord.Forbidden, discord.HTTPException): - await ctx.maybe_send_embed("Game role not configured and unable to generate one, cannot start") + await ctx.maybe_send_embed( + "Game role not configured and unable to generate one, cannot start" + ) self.roles = [] return False try: @@ -120,24 +133,33 @@ class Game: await player.member.add_roles(*[self.game_role]) except discord.Forbidden: await ctx.send( - "Unable to add role **{}**\nBot is missing `manage_roles` permissions".format(self.game_role.name)) + "Unable to add role **{}**\nBot is missing `manage_roles` permissions".format( + self.game_role.name + ) + ) return False await self.assign_roles() # Create category and channel with individual overwrites overwrite = { - self.guild.default_role: discord.PermissionOverwrite(read_messages=True, send_messages=False, - add_reactions=False), - self.guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True, add_reactions=True, - manage_messages=True, manage_channels=True, - manage_roles=True), - self.game_role: discord.PermissionOverwrite(read_messages=True, send_messages=True) + self.guild.default_role: discord.PermissionOverwrite( + read_messages=True, send_messages=False, add_reactions=False + ), + self.guild.me: discord.PermissionOverwrite( + read_messages=True, + send_messages=True, + add_reactions=True, + manage_messages=True, + manage_channels=True, + manage_roles=True, + ), + self.game_role: discord.PermissionOverwrite(read_messages=True, send_messages=True), } if self.channel_category is None: - self.channel_category = await self.guild.create_category("Werewolf Game", - overwrites=overwrite, - reason="(BOT) New game of werewolf") + self.channel_category = await self.guild.create_category( + "Werewolf Game", overwrites=overwrite, reason="(BOT) New game of werewolf" + ) else: # No need to modify categories pass # await self.channel_category.edit(name="🔴 Werewolf Game (ACTIVE)", reason="(BOT) New game of werewolf") @@ -147,20 +169,26 @@ class Game: # reason="(BOT) New game of werewolf") if self.village_channel is None: try: - self.village_channel = await self.guild.create_text_channel("🔵Werewolf", - overwrites=overwrite, - reason="(BOT) New game of werewolf", - category=self.channel_category) + self.village_channel = await self.guild.create_text_channel( + "🔵Werewolf", + overwrites=overwrite, + reason="(BOT) New game of werewolf", + category=self.channel_category, + ) except discord.Forbidden: - await ctx.send("Unable to create Game Channel and none was provided\n" - "Grant Bot appropriate permissions or assign a game_channel") + await ctx.send( + "Unable to create Game Channel and none was provided\n" + "Grant Bot appropriate permissions or assign a game_channel" + ) return False else: self.save_perms[self.village_channel] = self.village_channel.overwrites try: - await self.village_channel.edit(name="🔵Werewolf", - category=self.channel_category, - reason="(BOT) New game of werewolf") + await self.village_channel.edit( + name="🔵Werewolf", + category=self.channel_category, + reason="(BOT) New game of werewolf", + ) except discord.Forbidden as e: print("Unable to rename Game Channel") print(e) @@ -170,12 +198,14 @@ class Game: for target, ow in overwrite.items(): curr = self.village_channel.overwrites_for(target) curr.update(**{perm: value for perm, value in ow}) - await self.village_channel.set_permissions(target=target, - overwrite=curr, - reason="(BOT) New game of werewolf") + await self.village_channel.set_permissions( + target=target, overwrite=curr, reason="(BOT) New game of werewolf" + ) except discord.Forbidden: - await ctx.send("Unable to edit Game Channel permissions\n" - "Grant Bot appropriate permissions to manage permissions") + await ctx.send( + "Unable to edit Game Channel permissions\n" + "Grant Bot appropriate permissions to manage permissions" + ) return self.started = True # Assuming everything worked so far @@ -186,18 +216,25 @@ class Game: print("Channel id: " + channel_id) overwrite = { self.guild.default_role: discord.PermissionOverwrite(read_messages=False), - self.guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True, add_reactions=True, - manage_messages=True, manage_channels=True, - manage_roles=True) + self.guild.me: discord.PermissionOverwrite( + read_messages=True, + send_messages=True, + add_reactions=True, + manage_messages=True, + manage_channels=True, + manage_roles=True, + ), } for player in self.p_channels[channel_id]["players"]: overwrite[player.member] = discord.PermissionOverwrite(read_messages=True) - channel = await self.guild.create_text_channel(channel_id, - overwrites=overwrite, - reason="(BOT) WW game secret channel", - category=self.channel_category) + channel = await self.guild.create_text_channel( + channel_id, + overwrites=overwrite, + reason="(BOT) WW game secret channel", + category=self.channel_category, + ) self.p_channels[channel_id]["channel"] = channel @@ -216,8 +253,8 @@ class Game: async def _cycle(self): """ Each event calls the next event - - + + _at_day_start() _at_voted() @@ -225,7 +262,7 @@ class Game: _at_day_end() _at_night_begin() _at_night_end() - + and repeat with _at_day_start() again """ await self._at_day_start() @@ -237,7 +274,8 @@ class Game: return await self.village_channel.send( - embed=discord.Embed(title="Game is starting, please wait for setup to complete")) + embed=discord.Embed(title="Game is starting, please wait for setup to complete") + ) await self._notify(0) @@ -271,7 +309,9 @@ class Game: await asyncio.sleep(24) # 4 minute days FixMe to 120 later if check(): return - await self.village_channel.send(embed=discord.Embed(title="**Two minutes of daylight remain...**")) + await self.village_channel.send( + embed=discord.Embed(title="**Two minutes of daylight remain...**") + ) await asyncio.sleep(24) # 4 minute days FixMe to 120 later # Need a loop here to wait for trial to end (can_vote?) @@ -295,7 +335,10 @@ class Game: await self.speech_perms(self.village_channel, target.member) # Only target can talk await self.village_channel.send( - "**{} will be put to trial and has 30 seconds to defend themselves**".format(target.mention)) + "**{} will be put to trial and has 30 seconds to defend themselves**".format( + target.mention + ) + ) await asyncio.sleep(30) @@ -305,7 +348,8 @@ class Game: "Everyone will now vote whether to lynch {}\n" "👍 to save, 👎 to lynch\n" "*Majority rules, no-lynch on ties, " - "vote both or neither to abstain, 15 seconds to vote*".format(target.mention)) + "vote both or neither to abstain, 15 seconds to vote*".format(target.mention) + ) await message.add_reaction("👍") await message.add_reaction("👎") @@ -317,9 +361,9 @@ class Game: down_votes = sum(p for p in reaction_list if p.emoji == "👎" and not p.me) if down_votes > up_votes: - embed = discord.Embed(title="Vote Results", color=0xff0000) + embed = discord.Embed(title="Vote Results", color=0xFF0000) else: - embed = discord.Embed(title="Vote Results", color=0x80ff80) + embed = discord.Embed(title="Vote Results", color=0x80FF80) embed.add_field(name="👎", value="**{}**".format(up_votes), inline=True) embed.add_field(name="👍", value="**{}**".format(down_votes), inline=True) @@ -339,7 +383,8 @@ class Game: else: await self.village_channel.send( "**{}**/**{}** of today's votes have been used!\n" - "Nominate carefully..".format(self.used_votes, self.day_vote_count)) + "Nominate carefully..".format(self.used_votes, self.day_vote_count) + ) self.ongoing_vote = False @@ -373,7 +418,9 @@ class Game: await self.night_perms(self.village_channel) - await self.village_channel.send(embed=discord.Embed(title="**The sun sets on the village...**")) + await self.village_channel.send( + embed=discord.Embed(title="**The sun sets on the village...**") + ) await self._notify(5) await asyncio.sleep(5) @@ -385,9 +432,13 @@ class Game: await self._notify(6) await asyncio.sleep(12) # 2 minutes FixMe to 120 later - await self.village_channel.send(embed=discord.Embed(title="**Two minutes of night remain...**")) + await self.village_channel.send( + embed=discord.Embed(title="**Two minutes of night remain...**") + ) await asyncio.sleep(9) # 1.5 minutes FixMe to 90 later - await self.village_channel.send(embed=discord.Embed(title="**Thirty seconds until sunrise...**")) + await self.village_channel.send( + embed=discord.Embed(title="**Thirty seconds until sunrise...**") + ) await asyncio.sleep(3) # .5 minutes FixMe to 3 Later await self._at_night_end() @@ -413,10 +464,12 @@ class Game: role_order = [role for role in self.roles if role.action_list[event][1] == i] for role in role_order: tasks.append(asyncio.ensure_future(role.on_event(event, data), loop=self.loop)) - # VoteGroup priorities + # VoteGroup priorities vote_order = [vg for vg in self.vote_groups.values() if vg.action_list[event][1] == i] for vote_group in vote_order: - tasks.append(asyncio.ensure_future(vote_group.on_event(event, data), loop=self.loop)) + tasks.append( + asyncio.ensure_future(vote_group.on_event(event, data), loop=self.loop) + ) if tasks: await asyncio.gather(*tasks) # Run same-priority task simultaneously @@ -432,13 +485,17 @@ class Game: else: status = "*[Dead]*-" if with_roles or not player.alive: - embed.add_field(name="ID# **{}**".format(i), - value="{}{}-{}".format(status, player.member.display_name, str(player.role)), - inline=True) + embed.add_field( + name="ID# **{}**".format(i), + value="{}{}-{}".format(status, player.member.display_name, str(player.role)), + inline=True, + ) else: - embed.add_field(name="ID# **{}**".format(i), - value="{}{}".format(status, player.member.display_name), - inline=True) + embed.add_field( + name="ID# **{}**".format(i), + value="{}{}".format(status, player.member.display_name), + inline=True, + ) return await channel.send(embed=embed) @@ -479,10 +536,15 @@ class Game: await member.add_roles(*[self.game_role]) except discord.Forbidden: await channel.send( - "Unable to add role **{}**\nBot is missing `manage_roles` permissions".format(self.game_role.name)) + "Unable to add role **{}**\nBot is missing `manage_roles` permissions".format( + self.game_role.name + ) + ) - await channel.send("{} has been added to the game, " - "total players is **{}**".format(member.mention, len(self.players))) + await channel.send( + "{} has been added to the game, " + "total players is **{}**".format(member.mention, len(self.players)) + ) async def quit(self, member: discord.Member, channel: discord.TextChannel = None): """ @@ -499,7 +561,11 @@ class Game: else: self.players = [player for player in self.players if player.member != member] await member.remove_roles(*[self.game_role]) - await channel.send("{} chickened out, player count is now **{}**".format(member.mention, len(self.players))) + await channel.send( + "{} chickened out, player count is now **{}**".format( + member.mention, len(self.players) + ) + ) async def choose(self, ctx, data): """ @@ -598,14 +664,20 @@ class Game: required_votes = len([player for player in self.players if player.alive]) // 7 + 2 if self.vote_totals[target_id] < required_votes: - await self.village_channel.send("" - "{} has voted to put {} to trial. " - "{} more votes needed".format(author.mention, - target.member.mention, - required_votes - self.vote_totals[target_id])) + await self.village_channel.send( + "" + "{} has voted to put {} to trial. " + "{} more votes needed".format( + author.mention, + target.member.mention, + required_votes - self.vote_totals[target_id], + ) + ) else: self.vote_totals[target_id] = 0 - self.day_vote = {k: v for k, v in self.day_vote.items() if v != target_id} # Remove votes for this id + self.day_vote = { + k: v for k, v in self.day_vote.items() if v != target_id + } # Remove votes for this id await self._at_voted(target) async def eval_results(self, target, source=None, method=None): @@ -613,9 +685,9 @@ class Game: out = "**{ID}** - " + method return out.format(ID=target.id, target=target.member.display_name) else: - return "**{ID}** - {target} the {role} was found dead".format(ID=target.id, - target=target.member.display_name, - role=await target.role.get_role()) + return "**{ID}** - {target} the {role} was found dead".format( + ID=target.id, target=target.member.display_name, role=await target.role.get_role() + ) async def _quit(self, player): """ @@ -748,7 +820,9 @@ class Game: alive_players = [player for player in self.players if player.alive] if len(alive_players) <= 0: - await self.village_channel.send(embed=discord.Embed(title="**Everyone is dead! Game Over!**")) + await self.village_channel.send( + embed=discord.Embed(title="**Everyone is dead! Game Over!**") + ) self.game_over = True elif len(alive_players) == 1: self.game_over = True @@ -780,17 +854,19 @@ class Game: async def _announce_winners(self, winnerlist): await self.village_channel.send(self.game_role.mention) - embed = discord.Embed(title='Game Over', description='The Following Players have won!') + embed = discord.Embed(title="Game Over", description="The Following Players have won!") for player in winnerlist: embed.add_field(name=player.member.display_name, value=str(player.role), inline=True) - embed.set_thumbnail(url='https://emojipedia-us.s3.amazonaws.com/thumbs/160/twitter/134/trophy_1f3c6.png') + embed.set_thumbnail( + url="https://emojipedia-us.s3.amazonaws.com/thumbs/160/twitter/134/trophy_1f3c6.png" + ) await self.village_channel.send(embed=embed) await self.generate_targets(self.village_channel, True) async def _end_game(self): # Remove game_role access for potential archiving for now - reason = '(BOT) End of WW game' + reason = "(BOT) End of WW game" for obj in self.to_delete: print(obj) await obj.delete(reason=reason) @@ -798,8 +874,12 @@ class Game: try: await self.village_channel.edit(reason=reason, name="Werewolf") for target, overwrites in self.save_perms[self.village_channel]: - await self.village_channel.set_permissions(target, overwrite=overwrites, reason=reason) - await self.village_channel.set_permissions(self.game_role, overwrite=None, reason=reason) + await self.village_channel.set_permissions( + target, overwrite=overwrites, reason=reason + ) + await self.village_channel.set_permissions( + self.game_role, overwrite=None, reason=reason + ) except (discord.HTTPException, discord.NotFound, discord.errors.NotFound): pass diff --git a/werewolf/role.py b/werewolf/role.py index 3e4124d..af2d38b 100644 --- a/werewolf/role.py +++ b/werewolf/role.py @@ -1,31 +1,31 @@ class Role: """ Base Role class for werewolf game - + Category enrollment guide as follows (category property): Town: 1: Random, 2: Investigative, 3: Protective, 4: Government, 5: Killing, 6: Power (Special night action) - + Werewolf: 11: Random, 12: Deception, 15: Killing, 16: Support - + Neutral: 21: Benign, 22: Evil, 23: Killing - - + + Example category: category = [1, 5, 6] Could be Veteran category = [1, 5] Could be Bodyguard category = [11, 16] Could be Werewolf Silencer - - + + Action guide as follows (on_event function): _at_night_start 0. No Action 1. Detain actions (Jailer/Kidnapper) 2. Group discussions and choose targets - + _at_night_end 0. No Action 1. Self actions (Veteran) @@ -68,7 +68,7 @@ class Role: (self._at_day_end, 0), (self._at_night_start, 0), (self._at_night_end, 0), - (self._at_visit, 0) + (self._at_visit, 0), ] def __repr__(self): diff --git a/werewolf/votegroup.py b/werewolf/votegroup.py index bf07c8c..a754247 100644 --- a/werewolf/votegroup.py +++ b/werewolf/votegroup.py @@ -23,7 +23,7 @@ class VoteGroup: (self._at_day_end, 0), (self._at_night_start, 2), (self._at_night_end, 0), - (self._at_visit, 0) + (self._at_visit, 0), ] async def on_event(self, event, data): From 7e1a6e108ea804fc3a35601d8740cc555f3db6ed Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 18 Sep 2020 17:02:37 -0400 Subject: [PATCH 05/43] Logs mainly --- werewolf/builder.py | 3 +++ werewolf/game.py | 3 +++ werewolf/night_powers.py | 4 ++++ werewolf/player.py | 4 ++++ werewolf/role.py | 5 +++++ werewolf/votegroup.py | 5 +++++ werewolf/werewolf.py | 4 ++++ 7 files changed, 28 insertions(+) diff --git a/werewolf/builder.py b/werewolf/builder.py index 1648b45..4632a21 100644 --- a/werewolf/builder.py +++ b/werewolf/builder.py @@ -1,4 +1,5 @@ import bisect +import logging from collections import defaultdict from random import choice @@ -13,6 +14,8 @@ from .roles.vanillawerewolf import VanillaWerewolf from .roles.villager import Villager from redbot.core.utils.menus import menu, prev_page, next_page, close_menu +log = logging.getLogger("red.fox_v3.werewolf.builder") + # All roles in this list for iterating ROLE_LIST = sorted([Villager, Seer, VanillaWerewolf], key=lambda x: x.alignment) diff --git a/werewolf/game.py b/werewolf/game.py index 95807a1..7c6a5c8 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -1,4 +1,5 @@ import asyncio +import logging import random from typing import List, Any, Dict, Set, Union @@ -10,6 +11,8 @@ from .player import Player from .role import Role from .votegroup import VoteGroup +log = logging.getLogger("red.fox_v3.werewolf.game") + class Game: """ diff --git a/werewolf/night_powers.py b/werewolf/night_powers.py index b50929b..a39ad26 100644 --- a/werewolf/night_powers.py +++ b/werewolf/night_powers.py @@ -1,5 +1,9 @@ +import logging + from .role import Role +log = logging.getLogger("red.fox_v3.werewolf.night_powers") + def night_immune(role: Role): role.player.alive = True diff --git a/werewolf/player.py b/werewolf/player.py index c84d87f..8ee437e 100644 --- a/werewolf/player.py +++ b/werewolf/player.py @@ -1,5 +1,9 @@ +import logging + import discord +log = logging.getLogger("red.fox_v3.werewolf.player") + class Player: """ diff --git a/werewolf/role.py b/werewolf/role.py index af2d38b..8c4d989 100644 --- a/werewolf/role.py +++ b/werewolf/role.py @@ -1,3 +1,8 @@ +import logging + +log = logging.getLogger("red.fox_v3.werewolf.role") + + class Role: """ Base Role class for werewolf game diff --git a/werewolf/votegroup.py b/werewolf/votegroup.py index a754247..d60f451 100644 --- a/werewolf/votegroup.py +++ b/werewolf/votegroup.py @@ -1,3 +1,8 @@ +import logging + +log = logging.getLogger("red.fox_v3.werewolf.votegroup") + + class VoteGroup: """ Base VoteGroup class for werewolf game diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 1f8fc3f..c90982f 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -1,3 +1,5 @@ +import logging + import discord from redbot.core import Config, checks, commands from redbot.core.bot import Red @@ -13,6 +15,8 @@ from .builder import ( ) from .game import Game +log = logging.getLogger("red.fox_v3.werewolf") + class Werewolf(Cog): """ From fe1f11b2ebc1b27c9461d9a632aa69c520744acf Mon Sep 17 00:00:00 2001 From: bobloy Date: Sat, 19 Sep 2020 20:21:28 -0400 Subject: [PATCH 06/43] Maybe dispatch? WIP --- werewolf/game.py | 46 +++++++++++++++++++++------------ werewolf/role.py | 2 +- werewolf/roles/seer.py | 14 +++++++--- werewolf/roles/shifter.py | 2 +- werewolf/votegroups/wolfvote.py | 17 ++++++++---- werewolf/werewolf.py | 18 ++++++------- 6 files changed, 63 insertions(+), 36 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index 7c6a5c8..ba63211 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -5,6 +5,7 @@ from typing import List, Any, Dict, Set, Union import discord from redbot.core import commands +from redbot.core.bot import Red from .builder import parse_code from .player import Player @@ -34,10 +35,15 @@ class Game: "**Morning has arrived on day {}..**", ] + night_messages = [ + "**Dawn falls on day {}..****" + ] + day_vote_count = 3 def __init__( self, + bot: Red, guild: discord.Guild, role: discord.Role = None, category: discord.CategoryChannel = None, @@ -45,6 +51,7 @@ class Game: log_channel: discord.TextChannel = None, game_code=None, ): + self.bot = bot self.guild = guild self.game_code = game_code @@ -340,6 +347,9 @@ class Game: await self.village_channel.send( "**{} will be put to trial and has 30 seconds to defend themselves**".format( target.mention + ), + allowed_mentions=discord.AllowedMentions( + everyone=False, users=[target] ) ) @@ -347,11 +357,14 @@ class Game: await self.speech_perms(self.village_channel, target.member, undo=True) # No one can talk - message = await self.village_channel.send( + message: discord.Message = await self.village_channel.send( "Everyone will now vote whether to lynch {}\n" "👍 to save, 👎 to lynch\n" "*Majority rules, no-lynch on ties, " - "vote both or neither to abstain, 15 seconds to vote*".format(target.mention) + "vote both or neither to abstain, 15 seconds to vote*".format(target.mention), + allowed_mentions=discord.AllowedMentions( + everyone=False, users=[target] + ) ) await message.add_reaction("👍") @@ -442,7 +455,7 @@ class Game: await self.village_channel.send( embed=discord.Embed(title="**Thirty seconds until sunrise...**") ) - await asyncio.sleep(3) # .5 minutes FixMe to 3 Later + await asyncio.sleep(3) # .5 minutes FixMe to 30 Later await self._at_night_end() @@ -462,19 +475,20 @@ class Game: async def _notify(self, event, data=None): for i in range(1, 7): # action guide 1-6 (0 is no action) - tasks = [] - # Role priorities - role_order = [role for role in self.roles if role.action_list[event][1] == i] - for role in role_order: - tasks.append(asyncio.ensure_future(role.on_event(event, data), loop=self.loop)) - # VoteGroup priorities - vote_order = [vg for vg in self.vote_groups.values() if vg.action_list[event][1] == i] - for vote_group in vote_order: - tasks.append( - asyncio.ensure_future(vote_group.on_event(event, data), loop=self.loop) - ) - if tasks: - await asyncio.gather(*tasks) + self.bot.dispatch(f"red.fox.werewolf.{event}", data=data, priority=i) + # tasks = [] + # # Role priorities + # role_order = [role for role in self.roles if role.action_list[event][1] == i] + # for role in role_order: + # tasks.append(asyncio.ensure_future(role.on_event(event, data), loop=self.loop)) + # # VoteGroup priorities + # vote_order = [vg for vg in self.vote_groups.values() if vg.action_list[event][1] == i] + # for vote_group in vote_order: + # tasks.append( + # asyncio.ensure_future(vote_group.on_event(event, data), loop=self.loop) + # ) + # if tasks: + # await asyncio.gather(*tasks) # Run same-priority task simultaneously ############END Notify structure############ diff --git a/werewolf/role.py b/werewolf/role.py index 8c4d989..2a08f71 100644 --- a/werewolf/role.py +++ b/werewolf/role.py @@ -38,7 +38,7 @@ class Role: 3. Protection / Preempt actions (bodyguard/framer) 4. Non-disruptive actions (seer/silencer) 5. Disruptive actions (Killing) - 6. Role altering actions (Cult / Mason) + 6. Role altering actions (Cult / Mason / Shifter) """ rand_choice = False # Determines if it can be picked as a random role (False for unusually disruptive roles) diff --git a/werewolf/roles/seer.py b/werewolf/roles/seer.py index 35c8271..8d0fc1f 100644 --- a/werewolf/roles/seer.py +++ b/werewolf/roles/seer.py @@ -14,8 +14,10 @@ class Seer(Role): "Lynch players during the day with `[p]ww vote `\n" "Check for werewolves at night with `[p]ww choose `" ) - description = "A mystic in search of answers in a chaotic town.\n" \ - "Calls upon the cosmos to discern those of Lycan blood" + description = ( + "A mystic in search of answers in a chaotic town.\n" + "Calls upon the cosmos to discern those of Lycan blood" + ) def __init__(self, game): super().__init__(game) @@ -33,7 +35,7 @@ class Seer(Role): (self._at_day_end, 0), (self._at_night_start, 2), (self._at_night_end, 4), - (self._at_visit, 0) + (self._at_visit, 0), ] async def see_alignment(self, source=None): @@ -87,4 +89,8 @@ class Seer(Role): await super().choose(ctx, data) self.see_target, target = await pick_target(self, ctx, data) - await ctx.send("**You will attempt to see the role of {} tonight...**".format(target.member.display_name)) + await ctx.send( + "**You will attempt to see the role of {} tonight...**".format( + target.member.display_name + ) + ) diff --git a/werewolf/roles/shifter.py b/werewolf/roles/shifter.py index 4c550dc..8d4b4f5 100644 --- a/werewolf/roles/shifter.py +++ b/werewolf/roles/shifter.py @@ -37,7 +37,7 @@ class Shifter(Role): 3. Protection / Preempt actions (bodyguard/framer) 4. Non-disruptive actions (seer/silencer) 5. Disruptive actions (Killing) - 6. Role altering actions (Cult / Mason) + 6. Role altering actions (Cult / Mason / Shifter) """ rand_choice = False # Determines if it can be picked as a random role (False for unusually disruptive roles) diff --git a/werewolf/votegroups/wolfvote.py b/werewolf/votegroups/wolfvote.py index 9c068d5..fb98b20 100644 --- a/werewolf/votegroups/wolfvote.py +++ b/werewolf/votegroups/wolfvote.py @@ -13,7 +13,8 @@ class WolfVote(VoteGroup): kill_messages = [ "**{ID}** - {target} was mauled by wolves", - "**{ID}** - {target} was found torn to shreds"] + "**{ID}** - {target} was found torn to shreds", + ] def __init__(self, game, channel): super().__init__(game, channel) @@ -34,7 +35,7 @@ class WolfVote(VoteGroup): (self._at_day_end, 0), (self._at_night_start, 2), (self._at_night_end, 5), # Kill priority - (self._at_visit, 0) + (self._at_visit, 0), ] # async def on_event(self, event, data): @@ -75,7 +76,9 @@ class WolfVote(VoteGroup): await self.channel.send(mention_list) self.killer = random.choice(self.players) - await self.channel.send("{} has been selected as tonight's killer".format(self.killer.member.display_name)) + await self.channel.send( + "{} has been selected as tonight's killer".format(self.killer.member.display_name) + ) async def _at_night_end(self, data=None): if self.channel is None: @@ -90,7 +93,9 @@ class WolfVote(VoteGroup): print("Target id: {}\nKiller: {}".format(target_id, self.killer.member.display_name)) if target_id is not None and self.killer: await self.game.kill(target_id, self.killer, random.choice(self.kill_messages)) - await self.channel.send("**{} has left to complete the kill...**".format(self.killer.member.display_name)) + await self.channel.send( + "**{} has left to complete the kill...**".format(self.killer.member.display_name) + ) else: await self.channel.send("**No kill will be attempted tonight...**") @@ -117,4 +122,6 @@ class WolfVote(VoteGroup): self.vote_results[author.id] = target_id - await self.channel.send("{} has voted to kill {}".format(author.mention, target.member.display_name)) + await self.channel.send( + "{} has voted to kill {}".format(author.mention, target.member.display_name) + ) diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index c90982f..0c8374c 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -97,7 +97,7 @@ class Werewolf(Cog): @wwset.command(name="role") async def wwset_role(self, ctx: commands.Context, role: discord.Role = None): """ - Assign the game role + Set the game role This role should not be manually assigned """ if role is None: @@ -222,7 +222,7 @@ class Werewolf(Cog): await ctx.send("No game running, cannot start") if not await game.setup(ctx): - pass # Do something? + pass # ToDo something? @commands.guild_only() @ww.command(name="stop") @@ -230,10 +230,10 @@ class Werewolf(Cog): """ Stops the current game """ - if ctx.guild is None: - # Private message, can't get guild - await ctx.send("Cannot start game from PM!") - return + # if ctx.guild is None: + # # Private message, can't get guild + # await ctx.send("Cannot stop game from PM!") + # return if ctx.guild.id not in self.games or self.games[ctx.guild.id].game_over: await ctx.send("No game to stop") return @@ -354,11 +354,11 @@ class Werewolf(Cog): await ctx.send("Role ID not found") async def _get_game(self, ctx: commands.Context, game_code=None): - guild: discord.Guild = ctx.guild + guild: discord.Guild = getattr(ctx, "guild", None) if guild is None: # Private message, can't get guild - await ctx.send("Cannot start game from PM!") + await ctx.send("Cannot start game from DM!") return None if guild.id not in self.games or self.games[guild.id].game_over: await ctx.send("Starting a new game...") @@ -368,7 +368,7 @@ class Werewolf(Cog): await ctx.send("Cannot start a new game") return None - self.games[guild.id] = Game(guild, role, category, channel, log_channel, game_code) + self.games[guild.id] = Game(self.bot, guild, role, category, channel, log_channel, game_code) return self.games[guild.id] From 28bf2a73e15625564dbe2d28b0a2c19879f54ede Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 21 Sep 2020 11:17:59 -0400 Subject: [PATCH 07/43] Still going on events --- chatter/chat.py | 2 +- werewolf/game.py | 52 +++++++++++++++++++++++++++++------------------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/chatter/chat.py b/chatter/chat.py index ad8e37b..ef75bb8 100644 --- a/chatter/chat.py +++ b/chatter/chat.py @@ -434,7 +434,7 @@ class Chatter(Cog): else: await ctx.maybe_send_embed("Error occurred :(") - @commands.Cog.listener() + @Cog.listener() async def on_message_without_command(self, message: discord.Message): """ Credit to https://github.com/Twentysix26/26-Cogs/blob/master/cleverbot/cleverbot.py diff --git a/werewolf/game.py b/werewolf/game.py index ba63211..c3aa813 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -1,6 +1,7 @@ import asyncio import logging import random +from collections import deque from typing import List, Any, Dict, Set, Union import discord @@ -85,6 +86,8 @@ class Game: self.loop = asyncio.get_event_loop() + self.action_queue = deque() + # def __del__(self): # """ # Cleanup channels as necessary @@ -275,8 +278,14 @@ class Game: and repeat with _at_day_start() again """ - await self._at_day_start() - # Once cycle ends, this will trigger end_game + + self.action_queue.append(self._at_day_start()) + + while self.action_queue: + await self.action_queue.popleft() + # + # await self._at_day_start() + # # Once cycle ends, this will trigger end_game await self._end_game() # Handle open channels async def _at_game_start(self): # ID 0 @@ -331,7 +340,7 @@ class Game: if check(): return - await self._at_day_end() + self.action_queue.append(self._at_day_end()) async def _at_voted(self, target): # ID 2 if self.game_over: @@ -405,7 +414,7 @@ class Game: self.ongoing_vote = False if not self.can_vote: - await self._at_day_end() + self.action_queue.append(self._at_day_end()) else: await self.normal_perms(self.village_channel) # No point if about to be night @@ -440,7 +449,7 @@ class Game: await self._notify(5) await asyncio.sleep(5) - await self._at_night_start() + self.action_queue.append(self._at_night_start()) async def _at_night_start(self): # ID 6 if self.game_over: @@ -457,7 +466,7 @@ class Game: ) await asyncio.sleep(3) # .5 minutes FixMe to 30 Later - await self._at_night_end() + self.action_queue.append(self._at_night_end()) async def _at_night_end(self): # ID 7 if self.game_over: @@ -465,7 +474,7 @@ class Game: await self._notify(7) await asyncio.sleep(10) - await self._at_day_start() + self.action_queue.append(self._at_day_start()) async def _at_visit(self, target, source): # ID 8 if self.game_over: @@ -475,20 +484,21 @@ class Game: async def _notify(self, event, data=None): for i in range(1, 7): # action guide 1-6 (0 is no action) - self.bot.dispatch(f"red.fox.werewolf.{event}", data=data, priority=i) - # tasks = [] - # # Role priorities - # role_order = [role for role in self.roles if role.action_list[event][1] == i] - # for role in role_order: - # tasks.append(asyncio.ensure_future(role.on_event(event, data), loop=self.loop)) - # # VoteGroup priorities - # vote_order = [vg for vg in self.vote_groups.values() if vg.action_list[event][1] == i] - # for vote_group in vote_order: - # tasks.append( - # asyncio.ensure_future(vote_group.on_event(event, data), loop=self.loop) - # ) - # if tasks: - # await asyncio.gather(*tasks) + # self.bot.dispatch(f"red.fox.werewolf.{event}", data=data, priority=i) + # self.bot.extra_events + tasks = [] + # Role priorities + role_order = [role for role in self.roles if role.action_list[event][1] == i] + for role in role_order: + tasks.append(asyncio.ensure_future(role.on_event(event, data), loop=self.loop)) + # VoteGroup priorities + vote_order = [vg for vg in self.vote_groups.values() if vg.action_list[event][1] == i] + for vote_group in vote_order: + tasks.append( + asyncio.ensure_future(vote_group.on_event(event, data), loop=self.loop) + ) + if tasks: + await asyncio.gather(*tasks) # Run same-priority task simultaneously ############END Notify structure############ From 1723dc381d4a49fc5f2c14a666499967c8bd7f33 Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 21 Sep 2020 13:42:43 -0400 Subject: [PATCH 08/43] More listener --- werewolf/game.py | 183 ++++++++++++++++++++++++++++++++++++++--------- werewolf/role.py | 84 +++++++++++----------- 2 files changed, 192 insertions(+), 75 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index c3aa813..e9d3763 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -1,4 +1,5 @@ import asyncio +import inspect import logging import random from collections import deque @@ -36,9 +37,7 @@ class Game: "**Morning has arrived on day {}..**", ] - night_messages = [ - "**Dawn falls on day {}..****" - ] + night_messages = ["**Dawn falls on day {}..****"] day_vote_count = 3 @@ -87,6 +86,7 @@ class Game: self.loop = asyncio.get_event_loop() self.action_queue = deque() + self.listeners = {} # def __del__(self): # """ @@ -265,9 +265,7 @@ class Game: ############START Notify structure############ async def _cycle(self): """ - Each event calls the next event - - + Each event enqueues the next event _at_day_start() _at_voted() @@ -296,7 +294,7 @@ class Game: embed=discord.Embed(title="Game is starting, please wait for setup to complete") ) - await self._notify(0) + await self._notify("at_game_start") async def _at_day_start(self): # ID 1 if self.game_over: @@ -318,7 +316,7 @@ class Game: await self.generate_targets(self.village_channel) await self.day_perms(self.village_channel) - await self._notify(1) + await self._notify("at_day_start") await self._check_game_over() if self.game_over: @@ -346,7 +344,7 @@ class Game: if self.game_over: return data = {"player": target} - await self._notify(2, data) + await self._notify("at_voted", player=target) self.ongoing_vote = True @@ -357,9 +355,7 @@ class Game: "**{} will be put to trial and has 30 seconds to defend themselves**".format( target.mention ), - allowed_mentions=discord.AllowedMentions( - everyone=False, users=[target] - ) + allowed_mentions=discord.AllowedMentions(everyone=False, users=[target]), ) await asyncio.sleep(30) @@ -371,9 +367,7 @@ class Game: "👍 to save, 👎 to lynch\n" "*Majority rules, no-lynch on ties, " "vote both or neither to abstain, 15 seconds to vote*".format(target.mention), - allowed_mentions=discord.AllowedMentions( - everyone=False, users=[target] - ) + allowed_mentions=discord.AllowedMentions(everyone=False, users=[target]), ) await message.add_reaction("👍") @@ -422,13 +416,13 @@ class Game: if self.game_over: return data = {"player": target} - await self._notify(3, data) + await self._notify("at_kill", player=target) async def _at_hang(self, target): # ID 4 if self.game_over: return data = {"player": target} - await self._notify(4, data) + await self._notify("at_hang", player=target) async def _at_day_end(self): # ID 5 await self._check_game_over() @@ -447,14 +441,14 @@ class Game: embed=discord.Embed(title="**The sun sets on the village...**") ) - await self._notify(5) + await self._notify("at_day_end") await asyncio.sleep(5) self.action_queue.append(self._at_night_start()) async def _at_night_start(self): # ID 6 if self.game_over: return - await self._notify(6) + await self._notify("at_night_start") await asyncio.sleep(12) # 2 minutes FixMe to 120 later await self.village_channel.send( @@ -471,7 +465,7 @@ class Game: async def _at_night_end(self): # ID 7 if self.game_over: return - await self._notify(7) + await self._notify("at_night_end") await asyncio.sleep(10) self.action_queue.append(self._at_day_start()) @@ -480,25 +474,30 @@ class Game: if self.game_over: return data = {"target": target, "source": source} - await self._notify(8, data) + await self._notify("at_visit", target=target, source=source) - async def _notify(self, event, data=None): + async def _notify(self, event, **kwargs): for i in range(1, 7): # action guide 1-6 (0 is no action) + tasks = [] + for event in self.listeners.get(event, []): + tasks.append(asyncio.ensure_future(event(**kwargs), loop=self.loop)) + await asyncio.gather(*tasks) + # self.bot.dispatch(f"red.fox.werewolf.{event}", data=data, priority=i) # self.bot.extra_events - tasks = [] - # Role priorities - role_order = [role for role in self.roles if role.action_list[event][1] == i] - for role in role_order: - tasks.append(asyncio.ensure_future(role.on_event(event, data), loop=self.loop)) - # VoteGroup priorities - vote_order = [vg for vg in self.vote_groups.values() if vg.action_list[event][1] == i] - for vote_group in vote_order: - tasks.append( - asyncio.ensure_future(vote_group.on_event(event, data), loop=self.loop) - ) - if tasks: - await asyncio.gather(*tasks) + # tasks = [] + # # Role priorities + # role_order = [role for role in self.roles if role.action_list[event][1] == i] + # for role in role_order: + # tasks.append(asyncio.ensure_future(role.on_event(event, data), loop=self.loop)) + # # VoteGroup priorities + # vote_order = [vg for vg in self.vote_groups.values() if vg.action_list[event][1] == i] + # for vote_group in vote_order: + # tasks.append( + # asyncio.ensure_future(vote_group.on_event(event, data), loop=self.loop) + # ) + # if tasks: + # await asyncio.gather(*tasks) # Run same-priority task simultaneously ############END Notify structure############ @@ -911,3 +910,117 @@ class Game: pass # Optional dynamic channels/categories + + @classmethod + def wolflistener(cls, name=None): + """A decorator that marks a function as a listener. + + This is the cog equivalent of :meth:`.Bot.listen`. + + Parameters + ------------ + name: :class:`str` + The name of the event being listened to. If not provided, it + defaults to the function's name. + + Raises + -------- + TypeError + The function is not a coroutine function or a string was not passed as + the name. + """ + + if name is not None and not isinstance(name, str): + raise TypeError( + "Cog.listener expected str but received {0.__class__.__name__!r} instead.".format( + name + ) + ) + + def decorator(func): + actual = func + if isinstance(actual, staticmethod): + actual = actual.__func__ + if not inspect.iscoroutinefunction(actual): + raise TypeError("Listener function must be a coroutine function.") + actual.__werewolf_listener__ = True + to_assign = name or actual.__name__ + try: + actual.__cog_listener_names__.append(to_assign) + except AttributeError: + actual.__cog_listener_names__ = [to_assign] + # we have to return `func` instead of `actual` because + # we need the type to be `staticmethod` for the metaclass + # to pick it up but the metaclass unfurls the function and + # thus the assignments need to be on the actual function + return func + + return decorator + + def wolflisten(self, name=None): + """A decorator that registers another function as an external + event listener. Basically this allows you to listen to multiple + events from different places e.g. such as :func:`.on_ready` + + The functions being listened to must be a :ref:`coroutine `. + + Example + -------- + + .. code-block:: python3 + + @bot.listen() + async def on_message(message): + print('one') + + # in some other file... + + @bot.listen('on_message') + async def my_message(message): + print('two') + + Would print one and two in an unspecified order. + + Raises + ------- + TypeError + The function being listened to is not a coroutine. + """ + + def decorator(func): + self.add_wolflistener(func, name) + return func + + return decorator + + def add_wolflistener(self, func, name=None): + """The non decorator alternative to :meth:`.listen`. + + Parameters + ----------- + func: :ref:`coroutine ` + The function to call. + name: Optional[:class:`str`] + The name of the event to listen for. Defaults to ``func.__name__``. + + Example + -------- + + .. code-block:: python3 + + async def on_ready(): pass + async def my_message(message): pass + + bot.add_listener(on_ready) + bot.add_listener(my_message, 'on_message') + + """ + name = func.__name__ if name is None else name + + if not asyncio.iscoroutinefunction(func): + raise TypeError('Listeners must be coroutines') + + if name in self.listeners: + self.listeners[name].append(func) + else: + self.listeners[name] = [func] diff --git a/werewolf/role.py b/werewolf/role.py index 2a08f71..bdcec71 100644 --- a/werewolf/role.py +++ b/werewolf/role.py @@ -1,5 +1,8 @@ +import inspect import logging +from werewolf import Werewolf + log = logging.getLogger("red.fox_v3.werewolf.role") @@ -64,27 +67,27 @@ class Role: self.blocked = False self.properties = {} # Extra data for other roles (i.e. arsonist) - 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), - ] + # 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), + # ] def __repr__(self): return self.__class__.__name__ - async def on_event(self, event, data): - """ - See Game class for event guide - """ - - await self.action_list[event][0](data) + # async def on_event(self, event, data): + # """ + # See Game class for event guide + # """ + # + # await self.action_list[event][0](data) async def assign_player(self, player): """ @@ -124,35 +127,36 @@ class Role: """ return "Default" + @wolflistener("at_game_start") async def _at_game_start(self, data=None): if self.channel_id: await self.game.register_channel(self.channel_id, self) await self.player.send_dm(self.game_start_message) # Maybe embeds eventually - async def _at_day_start(self, data=None): - pass - - async def _at_voted(self, data=None): - pass - - async def _at_kill(self, data=None): - pass - - async def _at_hang(self, data=None): - pass - - async def _at_day_end(self, data=None): - pass - - async def _at_night_start(self, data=None): - pass - - async def _at_night_end(self, data=None): - pass - - async def _at_visit(self, data=None): - pass + # async def _at_day_start(self, data=None): + # pass + # + # async def _at_voted(self, data=None): + # pass + # + # async def _at_kill(self, data=None): + # pass + # + # async def _at_hang(self, data=None): + # pass + # + # async def _at_day_end(self, data=None): + # pass + # + # async def _at_night_start(self, data=None): + # pass + # + # async def _at_night_end(self, data=None): + # pass + # + # async def _at_visit(self, data=None): + # pass async def kill(self, source): """ From 8a3f45bdc1231b606f7dfecd096dbd2c670eda13 Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 21 Sep 2020 17:11:55 -0400 Subject: [PATCH 09/43] Listener structure major change Still need priority --- werewolf/game.py | 113 +++++++----------------------- werewolf/listener.py | 91 ++++++++++++++++++++++++ werewolf/role.py | 7 +- werewolf/roles/seer.py | 3 + werewolf/roles/shifter.py | 55 ++++++++------- werewolf/roles/vanillawerewolf.py | 24 ++++--- werewolf/votegroup.py | 67 +++++++++--------- 7 files changed, 200 insertions(+), 160 deletions(-) create mode 100644 werewolf/listener.py diff --git a/werewolf/game.py b/werewolf/game.py index e9d3763..df5d263 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -16,6 +16,7 @@ from .votegroup import VoteGroup log = logging.getLogger("red.fox_v3.werewolf.game") +HALF_DAY_LENGTH = 24 # FixMe: to 120 later for 4 minute days class Game: """ @@ -262,7 +263,7 @@ class Game: await asyncio.sleep(1) await asyncio.ensure_future(self._cycle()) # Start the loop - ############START Notify structure############ + # ###########START Notify structure############ async def _cycle(self): """ Each event enqueues the next event @@ -323,13 +324,13 @@ class Game: return self.can_vote = True - await asyncio.sleep(24) # 4 minute days FixMe to 120 later + await asyncio.sleep(HALF_DAY_LENGTH) # 4 minute days FixMe to 120 later if check(): return await self.village_channel.send( embed=discord.Embed(title="**Two minutes of daylight remain...**") ) - await asyncio.sleep(24) # 4 minute days FixMe to 120 later + await asyncio.sleep(HALF_DAY_LENGTH) # 4 minute days FixMe to 120 later # Need a loop here to wait for trial to end (can_vote?) while self.ongoing_vote: @@ -500,7 +501,7 @@ class Game: # await asyncio.gather(*tasks) # Run same-priority task simultaneously - ############END Notify structure############ + # ###########END Notify structure############ async def generate_targets(self, channel, with_roles=False): embed = discord.Embed(title="Remaining Players") @@ -911,89 +912,7 @@ class Game: # Optional dynamic channels/categories - @classmethod - def wolflistener(cls, name=None): - """A decorator that marks a function as a listener. - - This is the cog equivalent of :meth:`.Bot.listen`. - - Parameters - ------------ - name: :class:`str` - The name of the event being listened to. If not provided, it - defaults to the function's name. - - Raises - -------- - TypeError - The function is not a coroutine function or a string was not passed as - the name. - """ - - if name is not None and not isinstance(name, str): - raise TypeError( - "Cog.listener expected str but received {0.__class__.__name__!r} instead.".format( - name - ) - ) - - def decorator(func): - actual = func - if isinstance(actual, staticmethod): - actual = actual.__func__ - if not inspect.iscoroutinefunction(actual): - raise TypeError("Listener function must be a coroutine function.") - actual.__werewolf_listener__ = True - to_assign = name or actual.__name__ - try: - actual.__cog_listener_names__.append(to_assign) - except AttributeError: - actual.__cog_listener_names__ = [to_assign] - # we have to return `func` instead of `actual` because - # we need the type to be `staticmethod` for the metaclass - # to pick it up but the metaclass unfurls the function and - # thus the assignments need to be on the actual function - return func - - return decorator - - def wolflisten(self, name=None): - """A decorator that registers another function as an external - event listener. Basically this allows you to listen to multiple - events from different places e.g. such as :func:`.on_ready` - - The functions being listened to must be a :ref:`coroutine `. - - Example - -------- - - .. code-block:: python3 - - @bot.listen() - async def on_message(message): - print('one') - - # in some other file... - - @bot.listen('on_message') - async def my_message(message): - print('two') - - Would print one and two in an unspecified order. - - Raises - ------- - TypeError - The function being listened to is not a coroutine. - """ - - def decorator(func): - self.add_wolflistener(func, name) - return func - - return decorator - - def add_wolflistener(self, func, name=None): + def add_listener(self, func, name=None): """The non decorator alternative to :meth:`.listen`. Parameters @@ -1024,3 +943,23 @@ class Game: self.listeners[name].append(func) else: self.listeners[name] = [func] + + def remove_listener(self, func, name=None): + """Removes a listener from the pool of listeners. + + Parameters + ----------- + func + The function that was used as a listener to remove. + name: :class:`str` + The name of the event we want to remove. Defaults to + ``func.__name__``. + """ + + name = func.__name__ if name is None else name + + if name in self.listeners: + try: + self.listeners[name].remove(func) + except ValueError: + pass diff --git a/werewolf/listener.py b/werewolf/listener.py new file mode 100644 index 0000000..9c36400 --- /dev/null +++ b/werewolf/listener.py @@ -0,0 +1,91 @@ +import inspect + + +def wolflistener(name=None): + """A decorator that marks a function as a listener. + + This is the werewolf.Game equivalent of :meth:`.Cog.listener`. + + Parameters + ------------ + name: :class:`str` + The name of the event being listened to. If not provided, it + defaults to the function's name. + + Raises + -------- + TypeError + The function is not a coroutine function or a string was not passed as + the name. + """ + + if name is not None and not isinstance(name, str): + raise TypeError( + "Game.listener expected str but received {0.__class__.__name__!r} instead.".format( + name + ) + ) + + def decorator(func): + actual = func + if isinstance(actual, staticmethod): + actual = actual.__func__ + if not inspect.iscoroutinefunction(actual): + raise TypeError("Listener function must be a coroutine function.") + actual.__wolf_listener__ = True + to_assign = name or actual.__name__ + try: + actual.__wolf_listener_names__.append(to_assign) + except AttributeError: + actual.__wolf_listener_names__ = [to_assign] + # we have to return `func` instead of `actual` because + # we need the type to be `staticmethod` for the metaclass + # to pick it up but the metaclass unfurls the function and + # thus the assignments need to be on the actual function + return func + + return decorator + + +class WolfListenerMeta(type): + def __new__(mcs, cls, *args, **kwargs): + name, bases = args + + commands = {} + listeners = {} + need_at_msg = "Listeners must start with at_ (in method {0.__name__}.{1})" + + new_cls = super().__new__(cls, name, bases, **kwargs) + for base in reversed(new_cls.__mro__): + for elem, value in base.__dict__.items(): + if elem in listeners: + del listeners[elem] + + is_static_method = isinstance(value, staticmethod) + if is_static_method: + value = value.__func__ + if inspect.iscoroutinefunction(value): + try: + is_listener = getattr(value, "__wolf_listener__") + except AttributeError: + continue + else: + if not elem.startswith("at_"): + raise TypeError(need_at_msg.format(mcs, elem)) + listeners[elem] = value + + listeners_as_list = [] + for listener in listeners.values(): + for listener_name in listener.__wolf_listener_names__: + # I use __name__ instead of just storing the value so I can inject + # the self attribute when the time comes to add them to the bot + listeners_as_list.append((listener_name, listener.__name__)) + + new_cls.__wolf_listeners__ = listeners_as_list + return new_cls + + +class WolfListener(metaclass=WolfListenerMeta): + def __init__(self, game): + for name, method_name in self.__wolf_listeners__: + game.add_listener(getattr(self, method_name), name) diff --git a/werewolf/role.py b/werewolf/role.py index bdcec71..4ae10ad 100644 --- a/werewolf/role.py +++ b/werewolf/role.py @@ -1,12 +1,12 @@ import inspect import logging -from werewolf import Werewolf +from werewolf.listener import WolfListener, wolflistener log = logging.getLogger("red.fox_v3.werewolf.role") -class Role: +class Role(WolfListener): """ Base Role class for werewolf game @@ -28,7 +28,7 @@ class Role: category = [11, 16] Could be Werewolf Silencer - Action guide as follows (on_event function): + Action priority guide as follows (on_event function): _at_night_start 0. No Action 1. Detain actions (Jailer/Kidnapper) @@ -62,6 +62,7 @@ class Role: icon_url = None # Adding a URL here will enable a thumbnail of the role def __init__(self, game): + super().__init__(game) self.game = game self.player = None self.blocked = False diff --git a/werewolf/roles/seer.py b/werewolf/roles/seer.py index 8d0fc1f..603d197 100644 --- a/werewolf/roles/seer.py +++ b/werewolf/roles/seer.py @@ -1,3 +1,4 @@ +from ..listener import wolflistener from ..night_powers import pick_target from ..role import Role @@ -59,6 +60,7 @@ class Seer(Role): """ return "Villager" + @wolflistener("at_night_start") async def _at_night_start(self, data=None): if not self.player.alive: return @@ -66,6 +68,7 @@ class Seer(Role): await self.game.generate_targets(self.player.member) await self.player.send_dm("**Pick a target to see tonight**") + @wolflistener("at_night_end") async def _at_night_end(self, data=None): if self.see_target is None: if self.player.alive: diff --git a/werewolf/roles/shifter.py b/werewolf/roles/shifter.py index 8d4b4f5..85c3ec5 100644 --- a/werewolf/roles/shifter.py +++ b/werewolf/roles/shifter.py @@ -1,3 +1,4 @@ +from ..listener import wolflistener from ..night_powers import pick_target from ..role import Role @@ -5,31 +6,31 @@ from ..role import Role class Shifter(Role): """ Base Role class for werewolf game - + Category enrollment guide as follows (category property): Town: 1: Random, 2: Investigative, 3: Protective, 4: Government, 5: Killing, 6: Power (Special night action) - + Werewolf: 11: Random, 12: Deception, 15: Killing, 16: Support - + Neutral: 21: Benign, 22: Evil, 23: Killing - - + + Example category: category = [1, 5, 6] Could be Veteran category = [1, 5] Could be Bodyguard category = [11, 16] Could be Werewolf Silencer - - + + Action guide as follows (on_event function): _at_night_start 0. No Action 1. Detain actions (Jailer/Kidnapper) 2. Group discussions and choose targets - + _at_night_end 0. No Action 1. Self actions (Veteran) @@ -61,17 +62,17 @@ class Shifter(Role): super().__init__(game) self.shift_target = None - 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, 2), # Chooses targets - (self._at_night_end, 6), # Role Swap - (self._at_visit, 0) - ] + # 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, 2), # Chooses targets + # (self._at_night_end, 6), # Role Swap + # (self._at_visit, 0), + # ] async def see_alignment(self, source=None): """ @@ -94,14 +95,14 @@ class Shifter(Role): """ return "Shifter" + @wolflistener("at_night_start") async def _at_night_start(self, data=None): - await super()._at_night_start(data) self.shift_target = None await self.game.generate_targets(self.player.member) await self.player.send_dm("**Pick a target to shift into**") + @wolflistener("at_night_end") async def _at_night_end(self, data=None): - await super()._at_night_end(data) if self.shift_target is None: if self.player.alive: await self.player.send_dm("You will not use your powers tonight...") @@ -114,16 +115,22 @@ class Shifter(Role): # Roles have now been swapped - await self.player.send_dm("Your role has been stolen...\n" - "You are now a **Shifter**.") + await self.player.send_dm( + "Your role has been stolen...\n" "You are now a **Shifter**." + ) await self.player.send_dm(self.game_start_message) await target.send_dm(target.role.game_start_message) else: await self.player.send_dm("**Your shift failed...**") + async def choose(self, ctx, data): """Handle night actions""" await super().choose(ctx, data) self.shift_target, target = await pick_target(self, ctx, data) - await ctx.send("**You will attempt to see the role of {} tonight...**".format(target.member.display_name)) + await ctx.send( + "**You will attempt to see the role of {} tonight...**".format( + target.member.display_name + ) + ) diff --git a/werewolf/roles/vanillawerewolf.py b/werewolf/roles/vanillawerewolf.py index c8050da..5f7407b 100644 --- a/werewolf/roles/vanillawerewolf.py +++ b/werewolf/roles/vanillawerewolf.py @@ -1,3 +1,4 @@ +from ..listener import wolflistener from ..role import Role from ..votegroups.wolfvote import WolfVote @@ -19,17 +20,17 @@ class VanillaWerewolf(Role): 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) - ] + # 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): """ @@ -52,6 +53,7 @@ class VanillaWerewolf(Role): """ return "Werewolf" + @wolflistener("at_game_start") async def _at_game_start(self, data=None): if self.channel_id: print("Wolf has channel_id: " + self.channel_id) diff --git a/werewolf/votegroup.py b/werewolf/votegroup.py index d60f451..19ebd9e 100644 --- a/werewolf/votegroup.py +++ b/werewolf/votegroup.py @@ -1,9 +1,11 @@ import logging +from werewolf.listener import WolfListener, wolflistener + log = logging.getLogger("red.fox_v3.werewolf.votegroup") -class VoteGroup: +class VoteGroup(WolfListener): """ Base VoteGroup class for werewolf game Handles secret channels and group decisions @@ -13,57 +15,55 @@ class VoteGroup: channel_id = "" def __init__(self, game, channel): + super().__init__(game) self.game = game self.channel = channel self.players = [] self.vote_results = {} self.properties = {} # Extra data for other options - self.action_list = [ - (self._at_game_start, 1), # (Action, Priority) - (self._at_day_start, 0), - (self._at_voted, 0), - (self._at_kill, 1), - (self._at_hang, 1), - (self._at_day_end, 0), - (self._at_night_start, 2), - (self._at_night_end, 0), - (self._at_visit, 0), - ] - - async def on_event(self, event, data): - """ - See Game class for event guide - """ - - await self.action_list[event][0](data) - + # self.action_list = [ + # (self._at_game_start, 1), # (Action, Priority) + # (self._at_day_start, 0), + # (self._at_voted, 0), + # (self._at_kill, 1), + # (self._at_hang, 1), + # (self._at_day_end, 0), + # (self._at_night_start, 2), + # (self._at_night_end, 0), + # (self._at_visit, 0), + # ] + + # async def on_event(self, event, data): + # """ + # See Game class for event guide + # """ + # + # await self.action_list[event][0](data) + + @wolflistener("at_game_start") async def _at_game_start(self, data=None): await self.channel.send(" ".join(player.mention for player in self.players)) - async def _at_day_start(self, data=None): - pass - - async def _at_voted(self, data=None): - pass - + @wolflistener("at_kill") async def _at_kill(self, data=None): if data["player"] in self.players: self.players.remove(data["player"]) - async def _at_hang(self, data=None): - if data["player"] in self.players: - self.players.remove(data["player"]) - - async def _at_day_end(self, data=None): - pass + # Removed, only if they actually die + # @wolflistener("at_hang") + # async def _at_hang(self, data=None): + # if data["player"] in self.players: + # self.players.remove(data["player"]) + @wolflistener("at_night_start") async def _at_night_start(self, data=None): if self.channel is None: return await self.game.generate_targets(self.channel) + @wolflistener("at_night_end") async def _at_night_end(self, data=None): if self.channel is None: return @@ -78,9 +78,6 @@ class VoteGroup: # Do what you voted on pass - async def _at_visit(self, data=None): - pass - async def register_players(self, *players): """ Extend players by passed list From 06af229a62f1a81c57f2c233bd96f3dfea57b3e9 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 22 Sep 2020 16:23:26 -0400 Subject: [PATCH 10/43] non-relative imports --- werewolf/roles/shifter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/werewolf/roles/shifter.py b/werewolf/roles/shifter.py index 85c3ec5..6e2a850 100644 --- a/werewolf/roles/shifter.py +++ b/werewolf/roles/shifter.py @@ -1,6 +1,6 @@ -from ..listener import wolflistener -from ..night_powers import pick_target -from ..role import Role +from werewolf.listener import wolflistener +from werewolf.night_powers import pick_target +from werewolf.role import Role class Shifter(Role): From a2eaf555159682478398228a465e5de3b34c86fb Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 22 Sep 2020 16:46:38 -0400 Subject: [PATCH 11/43] Priority update for listeners --- werewolf/game.py | 73 +++++++++++++++---------------- werewolf/listener.py | 33 ++++++++++---- werewolf/night_powers.py | 2 +- werewolf/roles/seer.py | 32 +++++++------- werewolf/roles/vanillawerewolf.py | 7 ++- werewolf/roles/villager.py | 2 +- werewolf/votegroups/__init__.py | 1 + werewolf/votegroups/wolfvote.py | 2 +- werewolf/werewolf.py | 16 ++++--- 9 files changed, 92 insertions(+), 76 deletions(-) create mode 100644 werewolf/votegroups/__init__.py diff --git a/werewolf/game.py b/werewolf/game.py index df5d263..98475ba 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -9,10 +9,10 @@ import discord from redbot.core import commands from redbot.core.bot import Red -from .builder import parse_code -from .player import Player -from .role import Role -from .votegroup import VoteGroup +from werewolf.builder import parse_code +from werewolf.player import Player +from werewolf.role import Role +from werewolf.votegroup import VoteGroup log = logging.getLogger("red.fox_v3.werewolf.game") @@ -480,7 +480,7 @@ class Game: async def _notify(self, event, **kwargs): for i in range(1, 7): # action guide 1-6 (0 is no action) tasks = [] - for event in self.listeners.get(event, []): + for event in self.listeners.get(event, {}).get(i, []): tasks.append(asyncio.ensure_future(event(**kwargs), loop=self.loop)) await asyncio.gather(*tasks) @@ -912,26 +912,19 @@ class Game: # Optional dynamic channels/categories - def add_listener(self, func, name=None): - """The non decorator alternative to :meth:`.listen`. + def add_ww_listener(self, func, priority=0, name=None): + """Adds a listener from the pool of listeners. Parameters ----------- func: :ref:`coroutine ` The function to call. + priority: Optional[:class:`int`] + Priority of the listener. Defaults to 0 (no-action) name: Optional[:class:`str`] The name of the event to listen for. Defaults to ``func.__name__``. - - Example - -------- - - .. code-block:: python3 - - async def on_ready(): pass - async def my_message(message): pass - - bot.add_listener(on_ready) - bot.add_listener(my_message, 'on_message') + do_sort: Optional[:class:`bool`] + Whether or not to sort listeners after. Skip sorting during mass appending """ name = func.__name__ if name is None else name @@ -940,26 +933,32 @@ class Game: raise TypeError('Listeners must be coroutines') if name in self.listeners: - self.listeners[name].append(func) + if priority in self.listeners[name]: + self.listeners[name][priority].append(func) + else: + self.listeners[name][priority] = [func] else: - self.listeners[name] = [func] - - def remove_listener(self, func, name=None): - """Removes a listener from the pool of listeners. + self.listeners[name] = {priority: [func]} - Parameters - ----------- - func - The function that was used as a listener to remove. - name: :class:`str` - The name of the event we want to remove. Defaults to - ``func.__name__``. - """ + # self.listeners[name].sort(reverse=True) - name = func.__name__ if name is None else name - if name in self.listeners: - try: - self.listeners[name].remove(func) - except ValueError: - pass + # def remove_wolf_listener(self, func, name=None): + # """Removes a listener from the pool of listeners. + # + # Parameters + # ----------- + # func + # The function that was used as a listener to remove. + # name: :class:`str` + # The name of the event we want to remove. Defaults to + # ``func.__name__``. + # """ + # + # name = func.__name__ if name is None else name + # + # if name in self.listeners: + # try: + # self.listeners[name].remove(func) + # except ValueError: + # pass diff --git a/werewolf/listener.py b/werewolf/listener.py index 9c36400..e14994a 100644 --- a/werewolf/listener.py +++ b/werewolf/listener.py @@ -1,7 +1,7 @@ import inspect -def wolflistener(name=None): +def wolflistener(name=None, priority=0): """A decorator that marks a function as a listener. This is the werewolf.Game equivalent of :meth:`.Cog.listener`. @@ -11,6 +11,22 @@ def wolflistener(name=None): name: :class:`str` The name of the event being listened to. If not provided, it defaults to the function's name. + priority: :class:`int` + The priority of the listener. + Priority guide as follows: + _at_night_start + 0. No Action + 1. Detain actions (Jailer/Kidnapper) + 2. Group discussions and choose targets + + _at_night_end + 0. No Action + 1. Self actions (Veteran) + 2. Target switching and role blocks (bus driver, witch, escort) + 3. Protection / Preempt actions (bodyguard/framer) + 4. Non-disruptive actions (seer/silencer) + 5. Disruptive actions (Killing) + 6. Role altering actions (Cult / Mason / Shifter) Raises -------- @@ -32,12 +48,12 @@ def wolflistener(name=None): actual = actual.__func__ if not inspect.iscoroutinefunction(actual): raise TypeError("Listener function must be a coroutine function.") - actual.__wolf_listener__ = True + actual.__wolf_listener__ = priority to_assign = name or actual.__name__ try: - actual.__wolf_listener_names__.append(to_assign) + actual.__wolf_listener_names__.append((priority, to_assign)) except AttributeError: - actual.__wolf_listener_names__ = [to_assign] + actual.__wolf_listener_names__ = [(priority, to_assign)] # we have to return `func` instead of `actual` because # we need the type to be `staticmethod` for the metaclass # to pick it up but the metaclass unfurls the function and @@ -51,7 +67,6 @@ class WolfListenerMeta(type): def __new__(mcs, cls, *args, **kwargs): name, bases = args - commands = {} listeners = {} need_at_msg = "Listeners must start with at_ (in method {0.__name__}.{1})" @@ -76,10 +91,10 @@ class WolfListenerMeta(type): listeners_as_list = [] for listener in listeners.values(): - for listener_name in listener.__wolf_listener_names__: + for priority, listener_name in listener.__wolf_listener_names__: # I use __name__ instead of just storing the value so I can inject # the self attribute when the time comes to add them to the bot - listeners_as_list.append((listener_name, listener.__name__)) + listeners_as_list.append((priority, listener_name, listener.__name__)) new_cls.__wolf_listeners__ = listeners_as_list return new_cls @@ -87,5 +102,5 @@ class WolfListenerMeta(type): class WolfListener(metaclass=WolfListenerMeta): def __init__(self, game): - for name, method_name in self.__wolf_listeners__: - game.add_listener(getattr(self, method_name), name) + for priority, name, method_name in self.__wolf_listeners__: + game.add_ww_listener(getattr(self, method_name), priority, name) diff --git a/werewolf/night_powers.py b/werewolf/night_powers.py index a39ad26..ab82e87 100644 --- a/werewolf/night_powers.py +++ b/werewolf/night_powers.py @@ -1,6 +1,6 @@ import logging -from .role import Role +from werewolf.role import Role log = logging.getLogger("red.fox_v3.werewolf.night_powers") diff --git a/werewolf/roles/seer.py b/werewolf/roles/seer.py index 603d197..a7a9aa8 100644 --- a/werewolf/roles/seer.py +++ b/werewolf/roles/seer.py @@ -1,6 +1,6 @@ -from ..listener import wolflistener -from ..night_powers import pick_target -from ..role import Role +from werewolf.listener import wolflistener +from werewolf.night_powers import pick_target +from werewolf.role import Role class Seer(Role): @@ -27,17 +27,17 @@ class Seer(Role): # self.blocked = False # self.properties = {} # Extra data for other roles (i.e. arsonist) self.see_target = None - 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, 2), - (self._at_night_end, 4), - (self._at_visit, 0), - ] + # 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, 2), + # (self._at_night_end, 4), + # (self._at_visit, 0), + # ] async def see_alignment(self, source=None): """ @@ -60,7 +60,7 @@ class Seer(Role): """ return "Villager" - @wolflistener("at_night_start") + @wolflistener("at_night_start", priority=2) async def _at_night_start(self, data=None): if not self.player.alive: return @@ -68,7 +68,7 @@ class Seer(Role): await self.game.generate_targets(self.player.member) await self.player.send_dm("**Pick a target to see tonight**") - @wolflistener("at_night_end") + @wolflistener("at_night_end", priority=4) async def _at_night_end(self, data=None): if self.see_target is None: if self.player.alive: diff --git a/werewolf/roles/vanillawerewolf.py b/werewolf/roles/vanillawerewolf.py index 5f7407b..e6938eb 100644 --- a/werewolf/roles/vanillawerewolf.py +++ b/werewolf/roles/vanillawerewolf.py @@ -1,7 +1,6 @@ -from ..listener import wolflistener -from ..role import Role - -from ..votegroups.wolfvote import WolfVote +from werewolf.listener import wolflistener +from werewolf.role import Role +from werewolf.votegroups.wolfvote import WolfVote class VanillaWerewolf(Role): diff --git a/werewolf/roles/villager.py b/werewolf/roles/villager.py index bda51d2..040e34d 100644 --- a/werewolf/roles/villager.py +++ b/werewolf/roles/villager.py @@ -1,4 +1,4 @@ -from ..role import Role +from werewolf.role import Role class Villager(Role): diff --git a/werewolf/votegroups/__init__.py b/werewolf/votegroups/__init__.py new file mode 100644 index 0000000..03abc1b --- /dev/null +++ b/werewolf/votegroups/__init__.py @@ -0,0 +1 @@ +from .wolfvote import WolfVote \ No newline at end of file diff --git a/werewolf/votegroups/wolfvote.py b/werewolf/votegroups/wolfvote.py index fb98b20..f990eef 100644 --- a/werewolf/votegroups/wolfvote.py +++ b/werewolf/votegroups/wolfvote.py @@ -1,6 +1,6 @@ import random -from ..votegroup import VoteGroup +from werewolf.votegroup import VoteGroup class WolfVote(VoteGroup): diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 0c8374c..abed258 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -6,14 +6,14 @@ from redbot.core.bot import Red from redbot.core.commands import Cog from redbot.core.utils.menus import DEFAULT_CONTROLS, menu -from .builder import ( +from werewolf.builder import ( GameBuilder, role_from_alignment, role_from_category, role_from_id, role_from_name, ) -from .game import Game +from werewolf.game import Game log = logging.getLogger("red.fox_v3.werewolf") @@ -81,8 +81,8 @@ class Werewolf(Cog): """ Lists current guild settings """ - success, role, category, channel, log_channel = await self._get_settings(ctx) - if not success: + valid, role, category, channel, log_channel = await self._get_settings(ctx) + if not valid: await ctx.send("Failed to get settings") return None @@ -362,13 +362,15 @@ class Werewolf(Cog): return None if guild.id not in self.games or self.games[guild.id].game_over: await ctx.send("Starting a new game...") - success, role, category, channel, log_channel = await self._get_settings(ctx) + valid, role, category, channel, log_channel = await self._get_settings(ctx) - if not success: + if not valid: await ctx.send("Cannot start a new game") return None - self.games[guild.id] = Game(self.bot, guild, role, category, channel, log_channel, game_code) + self.games[guild.id] = Game( + self.bot, guild, role, category, channel, log_channel, game_code + ) return self.games[guild.id] From 7109471c35194f284ea8f58a107b6ed89c05a37c Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 22 Sep 2020 17:30:58 -0400 Subject: [PATCH 12/43] WIP listeners, switch to f strings, and overall rewrite --- werewolf/builder.py | 16 +++---- werewolf/game.py | 73 ++++++++++++++++--------------- werewolf/listener.py | 10 ++--- werewolf/player.py | 2 +- werewolf/role.py | 2 +- werewolf/roles/seer.py | 8 ++-- werewolf/roles/shifter.py | 8 ++-- werewolf/roles/vanillawerewolf.py | 8 +++- werewolf/roles/villager.py | 4 ++ werewolf/votegroups/wolfvote.py | 7 ++- werewolf/werewolf.py | 2 +- 11 files changed, 80 insertions(+), 60 deletions(-) diff --git a/werewolf/builder.py b/werewolf/builder.py index 4632a21..ca90eca 100644 --- a/werewolf/builder.py +++ b/werewolf/builder.py @@ -49,7 +49,7 @@ CATEGORY_COUNT = [] def role_embed(idx, role, color): embed = discord.Embed( - title="**{}** - {}".format(idx, str(role.__name__)), + title=f"**{idx}** - {role.__name__}", description=role.game_start_message, color=color, ) @@ -82,7 +82,7 @@ def setup(): if 0 < k <= 6: ROLE_PAGES.append( discord.Embed( - title="RANDOM:Town Role", description="Town {}".format(v), color=0x008000 + title="RANDOM:Town Role", description=f"Town {v}", color=0x008000 ) ) CATEGORY_COUNT.append(k) @@ -95,7 +95,7 @@ def setup(): ROLE_PAGES.append( discord.Embed( title="RANDOM:Werewolf Role", - description="Werewolf {}".format(v), + description=f"Werewolf {v}", color=0xFF0000, ) ) @@ -107,7 +107,7 @@ def setup(): if 20 < k <= 26: ROLE_PAGES.append( discord.Embed( - title="RANDOM:Neutral Role", description="Neutral {}".format(v), color=0xC0C0C0 + title=f"RANDOM:Neutral Role", description="Neutral {v}", color=0xC0C0C0 ) ) CATEGORY_COUNT.append(k) @@ -306,14 +306,14 @@ def say_role_list(code_list, rand_roles): for role in rand_roles: if 0 < role <= 6: - role_dict["Town {}".format(ROLE_CATEGORIES[role])] += 1 + role_dict[f"Town {ROLE_CATEGORIES[role]}"] += 1 if 10 < role <= 16: - role_dict["Werewolf {}".format(ROLE_CATEGORIES[role])] += 1 + role_dict[f"Werewolf {ROLE_CATEGORIES[role]}"] += 1 if 20 < role <= 26: - role_dict["Neutral {}".format(ROLE_CATEGORIES[role])] += 1 + role_dict[f"Neutral {ROLE_CATEGORIES[role]}"] += 1 for k, v in role_dict.items(): - embed.add_field(name=k, value="Count: {}".format(v), inline=True) + embed.add_field(name=k, value=f"Count: {v}", inline=True) return embed diff --git a/werewolf/game.py b/werewolf/game.py index 98475ba..7372923 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -18,6 +18,7 @@ log = logging.getLogger("red.fox_v3.werewolf.game") HALF_DAY_LENGTH = 24 # FixMe: to 120 later for 4 minute days + class Game: """ Base class to run a single game of Werewolf @@ -119,10 +120,10 @@ class Game: if len(self.players) != len(self.roles): await ctx.maybe_send_embed( - "Player count does not match role count, cannot start\n" - "Currently **{} / {}**\n" - "Use `{}ww code` to pick a new game" - "".format(len(self.players), len(self.roles), ctx.prefix) + f"Player count does not match role count, cannot start\n" + f"Currently **{len(self.players)} / {len(self.roles)}**\n" + f"Use `{ctx.prefix}ww code` to pick a game setup\n" + f"Use `{ctx.prefix}buildgame` to generate a new game" ) self.roles = [] return False @@ -147,9 +148,7 @@ class Game: await player.member.add_roles(*[self.game_role]) except discord.Forbidden: await ctx.send( - "Unable to add role **{}**\nBot is missing `manage_roles` permissions".format( - self.game_role.name - ) + f"Unable to add role **{self.game_role.name}**\nBot is missing `manage_roles` permissions" ) return False @@ -353,9 +352,7 @@ class Game: await self.speech_perms(self.village_channel, target.member) # Only target can talk await self.village_channel.send( - "**{} will be put to trial and has 30 seconds to defend themselves**".format( - target.mention - ), + f"**{target.mention} will be put to trial and has 30 seconds to defend themselves**", allowed_mentions=discord.AllowedMentions(everyone=False, users=[target]), ) @@ -364,10 +361,10 @@ class Game: await self.speech_perms(self.village_channel, target.member, undo=True) # No one can talk message: discord.Message = await self.village_channel.send( - "Everyone will now vote whether to lynch {}\n" + f"Everyone will now vote whether to lynch {target.mention}\n" "👍 to save, 👎 to lynch\n" "*Majority rules, no-lynch on ties, " - "vote both or neither to abstain, 15 seconds to vote*".format(target.mention), + "vote both or neither to abstain, 15 seconds to vote*", allowed_mentions=discord.AllowedMentions(everyone=False, users=[target]), ) @@ -385,25 +382,31 @@ class Game: else: embed = discord.Embed(title="Vote Results", color=0x80FF80) - embed.add_field(name="👎", value="**{}**".format(up_votes), inline=True) - embed.add_field(name="👍", value="**{}**".format(down_votes), inline=True) + embed.add_field(name="👎", value=f"**{up_votes}**", inline=True) + embed.add_field(name="👍", value=f"**{down_votes}**", inline=True) await self.village_channel.send(embed=embed) if down_votes > up_votes: - await self.village_channel.send("**Voted to lynch {}!**".format(target.mention)) + await self.village_channel.send( + f"**Voted to lynch {target.mention}!**", + allowed_mentions=discord.AllowedMentions(everyone=False, users=[target]), + ) await self.lynch(target) self.can_vote = False else: - await self.village_channel.send("**{} has been spared!**".format(target.mention)) + await self.village_channel.send( + f"**{target.mention} has been spared!**", + allowed_mentions=discord.AllowedMentions(everyone=False, users=[target]), + ) if self.used_votes >= self.day_vote_count: await self.village_channel.send("**All votes have been used! Day is now over!**") self.can_vote = False else: await self.village_channel.send( - "**{}**/**{}** of today's votes have been used!\n" - "Nominate carefully..".format(self.used_votes, self.day_vote_count) + f"**{self.used_votes}**/**{self.day_vote_count}** of today's votes have been used!\n" + "Nominate carefully.." ) self.ongoing_vote = False @@ -513,14 +516,14 @@ class Game: status = "*[Dead]*-" if with_roles or not player.alive: embed.add_field( - name="ID# **{}**".format(i), - value="{}{}-{}".format(status, player.member.display_name, str(player.role)), + name=f"ID# **{i}**", + value=f"{status}{player.member.display_name}-{player.role}", inline=True, ) else: embed.add_field( - name="ID# **{}**".format(i), - value="{}{}".format(status, player.member.display_name), + name=f"ID# **{i}**", + value=f"{status}{player.member.display_name}", inline=True, ) @@ -553,7 +556,7 @@ class Game: return if await self.get_player_by_member(member) is not None: - await channel.send("{} is already in the game!".format(member.mention)) + await channel.send(f"{member.display_name} is already in the game!") return self.players.append(Player(member)) @@ -563,14 +566,12 @@ class Game: await member.add_roles(*[self.game_role]) except discord.Forbidden: await channel.send( - "Unable to add role **{}**\nBot is missing `manage_roles` permissions".format( - self.game_role.name - ) + f"Unable to add role **{self.game_role.name}**\nBot is missing `manage_roles` permissions" ) await channel.send( - "{} has been added to the game, " - "total players is **{}**".format(member.mention, len(self.players)) + f"{member.display_name} has been added to the game, " + f"total players is **{len(self.players)}**" ) async def quit(self, member: discord.Member, channel: discord.TextChannel = None): @@ -584,14 +585,16 @@ class Game: if self.started: await self._quit(player) - await channel.send("{} has left the game".format(member.mention)) + await channel.send( + f"{member.mention} has left the game", + allowed_mentions=discord.AllowedMentions(everyone=False, users=[member]), + ) else: self.players = [player for player in self.players if player.member != member] await member.remove_roles(*[self.game_role]) await channel.send( - "{} chickened out, player count is now **{}**".format( - member.mention, len(self.players) - ) + f"{member.mention} chickened out, player count is now **{len(self.players)}**", + allowed_mentions=discord.AllowedMentions(everyone=False, users=[member]), ) async def choose(self, ctx, data): @@ -698,7 +701,8 @@ class Game: author.mention, target.member.mention, required_votes - self.vote_totals[target_id], - ) + ), + allowed_mentions=discord.AllowedMentions(everyone=False, users=[author, target]), ) else: self.vote_totals[target_id] = 0 @@ -930,7 +934,7 @@ class Game: name = func.__name__ if name is None else name if not asyncio.iscoroutinefunction(func): - raise TypeError('Listeners must be coroutines') + raise TypeError("Listeners must be coroutines") if name in self.listeners: if priority in self.listeners[name]: @@ -942,7 +946,6 @@ class Game: # self.listeners[name].sort(reverse=True) - # def remove_wolf_listener(self, func, name=None): # """Removes a listener from the pool of listeners. # diff --git a/werewolf/listener.py b/werewolf/listener.py index e14994a..29ef7dd 100644 --- a/werewolf/listener.py +++ b/werewolf/listener.py @@ -64,13 +64,13 @@ def wolflistener(name=None, priority=0): class WolfListenerMeta(type): - def __new__(mcs, cls, *args, **kwargs): - name, bases = args + def __new__(mcs, *args, **kwargs): + name, bases, attrs = args listeners = {} need_at_msg = "Listeners must start with at_ (in method {0.__name__}.{1})" - new_cls = super().__new__(cls, name, bases, **kwargs) + new_cls = super().__new__(mcs, name, bases, attrs, **kwargs) for base in reversed(new_cls.__mro__): for elem, value in base.__dict__.items(): if elem in listeners: @@ -85,8 +85,8 @@ class WolfListenerMeta(type): except AttributeError: continue else: - if not elem.startswith("at_"): - raise TypeError(need_at_msg.format(mcs, elem)) + # if not elem.startswith("at_"): + # raise TypeError(need_at_msg.format(base, elem)) listeners[elem] = value listeners_as_list = [] diff --git a/werewolf/player.py b/werewolf/player.py index 8ee437e..201b781 100644 --- a/werewolf/player.py +++ b/werewolf/player.py @@ -34,4 +34,4 @@ class Player: try: await self.member.send(message) # Lets do embeds later except discord.Forbidden: - await self.role.game.village_channel.send("Couldn't DM {}, uh oh".format(self.mention)) + await self.role.game.village_channel.send(f"Couldn't DM {self.mention}, uh oh", allowed_mentions=discord.AllowedMentions(users=[self.member])) diff --git a/werewolf/role.py b/werewolf/role.py index 4ae10ad..0693f46 100644 --- a/werewolf/role.py +++ b/werewolf/role.py @@ -128,7 +128,7 @@ class Role(WolfListener): """ return "Default" - @wolflistener("at_game_start") + @wolflistener("at_game_start", priority=1) async def _at_game_start(self, data=None): if self.channel_id: await self.game.register_channel(self.channel_id, self) diff --git a/werewolf/roles/seer.py b/werewolf/roles/seer.py index a7a9aa8..f01c5c2 100644 --- a/werewolf/roles/seer.py +++ b/werewolf/roles/seer.py @@ -1,7 +1,11 @@ +import logging + from werewolf.listener import wolflistener from werewolf.night_powers import pick_target from werewolf.role import Role +log = logging.getLogger("red.fox_v3.werewolf.role.seer") + class Seer(Role): rand_choice = True # Determines if it can be picked as a random role (False for unusually disruptive roles) @@ -93,7 +97,5 @@ class Seer(Role): self.see_target, target = await pick_target(self, ctx, data) await ctx.send( - "**You will attempt to see the role of {} tonight...**".format( - target.member.display_name - ) + f"**You will attempt to see the role of {target.member.display_name} tonight...**" ) diff --git a/werewolf/roles/shifter.py b/werewolf/roles/shifter.py index 6e2a850..8f93d76 100644 --- a/werewolf/roles/shifter.py +++ b/werewolf/roles/shifter.py @@ -1,7 +1,11 @@ +import logging + from werewolf.listener import wolflistener from werewolf.night_powers import pick_target from werewolf.role import Role +log = logging.getLogger("red.fox_v3.werewolf.role.shifter") + class Shifter(Role): """ @@ -130,7 +134,5 @@ class Shifter(Role): self.shift_target, target = await pick_target(self, ctx, data) await ctx.send( - "**You will attempt to see the role of {} tonight...**".format( - target.member.display_name - ) + f"**You will attempt to see the role of {target.member.display_name} tonight...**" ) diff --git a/werewolf/roles/vanillawerewolf.py b/werewolf/roles/vanillawerewolf.py index e6938eb..db70eb5 100644 --- a/werewolf/roles/vanillawerewolf.py +++ b/werewolf/roles/vanillawerewolf.py @@ -1,7 +1,11 @@ +import logging + from werewolf.listener import wolflistener from werewolf.role import Role from werewolf.votegroups.wolfvote import WolfVote +log = logging.getLogger("red.fox_v3.werewolf.role.vanillawerewolf") + class VanillaWerewolf(Role): rand_choice = True @@ -56,7 +60,9 @@ class VanillaWerewolf(Role): async def _at_game_start(self, data=None): if self.channel_id: print("Wolf has channel_id: " + self.channel_id) - await self.game.register_channel(self.channel_id, self, WolfVote) # Add VoteGroup WolfVote + await self.game.register_channel( + self.channel_id, self, WolfVote + ) # Add VoteGroup WolfVote await self.player.send_dm(self.game_start_message) diff --git a/werewolf/roles/villager.py b/werewolf/roles/villager.py index 040e34d..f225e0d 100644 --- a/werewolf/roles/villager.py +++ b/werewolf/roles/villager.py @@ -1,5 +1,9 @@ +import logging + from werewolf.role import Role +log = logging.getLogger("red.fox_v3.werewolf.role.villager") + class Villager(Role): rand_choice = True # Determines if it can be picked as a random role (False for unusually disruptive roles) diff --git a/werewolf/votegroups/wolfvote.py b/werewolf/votegroups/wolfvote.py index f990eef..0823d77 100644 --- a/werewolf/votegroups/wolfvote.py +++ b/werewolf/votegroups/wolfvote.py @@ -1,7 +1,10 @@ +import logging import random from werewolf.votegroup import VoteGroup +log = logging.getLogger("red.fox_v3.werewolf.votegroup.wolfvote") + class WolfVote(VoteGroup): """ @@ -77,7 +80,7 @@ class WolfVote(VoteGroup): self.killer = random.choice(self.players) await self.channel.send( - "{} has been selected as tonight's killer".format(self.killer.member.display_name) + f"{self.killer.member.display_name} has been selected as tonight's killer" ) async def _at_night_end(self, data=None): @@ -90,7 +93,7 @@ class WolfVote(VoteGroup): if vote_list: target_id = max(set(vote_list), key=vote_list.count) - print("Target id: {}\nKiller: {}".format(target_id, self.killer.member.display_name)) + log.debug("Target id: {target_id}\nKiller: {self.killer.member.display_name}") if target_id is not None and self.killer: await self.game.kill(target_id, self.killer, random.choice(self.kill_messages)) await self.channel.send( diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index abed258..70574d2 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -62,7 +62,7 @@ class Werewolf(Cog): code = await gb.build_game(ctx) if code != "": - await ctx.send("Your game code is **{}**".format(code)) + await ctx.send(f"Your game code is **{code}**") else: await ctx.send("No code generated") From 29aa4930333d96f97049e88fb3b6eb1820273448 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 22 Sep 2020 17:50:05 -0400 Subject: [PATCH 13/43] Better checking of valid settings --- werewolf/werewolf.py | 46 +++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 70574d2..677e5c0 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -82,15 +82,18 @@ class Werewolf(Cog): Lists current guild settings """ valid, role, category, channel, log_channel = await self._get_settings(ctx) - if not valid: - await ctx.send("Failed to get settings") - return None + # if not valid: + # await ctx.send("Failed to get settings") + # return None - embed = discord.Embed(title="Current Guild Settings") + embed = discord.Embed( + title="Current Guild Settings", description=f"Valid: {valid}", color=0xFF0000 + ) embed.add_field(name="Role", value=str(role)) embed.add_field(name="Category", value=str(category)) embed.add_field(name="Channel", value=str(channel)) embed.add_field(name="Log Channel", value=str(log_channel)) + await ctx.send(embed=embed) @commands.guild_only() @@ -391,23 +394,30 @@ class Werewolf(Cog): if role_id is not None: role = discord.utils.get(guild.roles, id=role_id) - if role is None: - await ctx.send("Game Role is invalid") - return False, None, None, None, None + # if role is None: + # # await ctx.send("Game Role is invalid") + # return False, None, None, None, None if category_id is not None: category = discord.utils.get(guild.categories, id=category_id) - if category is None: - await ctx.send("Game Category is invalid") - return False, None, None, None, None + # if category is None: + # # await ctx.send("Game Category is invalid") + # return False, role, None, None, None if channel_id is not None: channel = discord.utils.get(guild.text_channels, id=channel_id) - if channel is None: - await ctx.send("Village Channel is invalid") - return False, None, None, None, None + # if channel is None: + # # await ctx.send("Village Channel is invalid") + # return False, role, category, None, None + if log_channel_id is not None: log_channel = discord.utils.get(guild.text_channels, id=log_channel_id) - if log_channel is None: - await ctx.send("Log Channel is invalid") - return False, None, None, None, None - - return True, role, category, channel, log_channel + # if log_channel is None: + # # await ctx.send("Log Channel is invalid") + # return False, None, None, None, None + + return ( + role is not None and category is not None and channel is not None, + role, + category, + channel, + log_channel, + ) From 98ae481d14e5851201ec740da530c69f185fe92a Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 22 Sep 2020 17:53:54 -0400 Subject: [PATCH 14/43] Show valid settings correctly --- werewolf/werewolf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 677e5c0..a011ccb 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -87,7 +87,9 @@ class Werewolf(Cog): # return None embed = discord.Embed( - title="Current Guild Settings", description=f"Valid: {valid}", color=0xFF0000 + title="Current Guild Settings", + description=f"Valid: {valid}", + color=0x008000 if valid else 0xFF0000, ) embed.add_field(name="Role", value=str(role)) embed.add_field(name="Category", value=str(category)) From 84ed2728e7ce29f3e97282b43f8af0b8776a742e Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 24 Sep 2020 13:36:42 -0400 Subject: [PATCH 15/43] switch to log --- werewolf/game.py | 16 ++++++++-------- werewolf/role.py | 2 +- werewolf/roles/vanillawerewolf.py | 4 ++-- werewolf/werewolf.py | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index 7372923..45f0cbd 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -203,8 +203,7 @@ class Game: reason="(BOT) New game of werewolf", ) except discord.Forbidden as e: - print("Unable to rename Game Channel") - print(e) + log.exception("Unable to rename Game Channel") await ctx.send("Unable to rename Game Channel, ignoring") try: @@ -222,11 +221,11 @@ class Game: return self.started = True # Assuming everything worked so far - print("Pre at_game_start") + log.debug("Pre at_game_start") await self._at_game_start() # This will queue channels and votegroups to be made - print("Post at_game_start") + log.debug("Post at_game_start") for channel_id in self.p_channels: - print("Channel id: " + channel_id) + log.debug("Setup Channel id: " + channel_id) overwrite = { self.guild.default_role: discord.PermissionOverwrite(read_messages=False), self.guild.me: discord.PermissionOverwrite( @@ -258,7 +257,7 @@ class Game: self.vote_groups[channel_id] = vote_group - print("Pre-cycle") + log.debug("Pre-cycle") await asyncio.sleep(1) await asyncio.ensure_future(self._cycle()) # Start the loop @@ -566,7 +565,8 @@ class Game: await member.add_roles(*[self.game_role]) except discord.Forbidden: await channel.send( - f"Unable to add role **{self.game_role.name}**\nBot is missing `manage_roles` permissions" + f"Unable to add role **{self.game_role.name}**\n" + f"Bot is missing `manage_roles` permissions" ) await channel.send( @@ -899,7 +899,7 @@ class Game: # Remove game_role access for potential archiving for now reason = "(BOT) End of WW game" for obj in self.to_delete: - print(obj) + log.debug(f"End_game: Deleting object {obj}") await obj.delete(reason=reason) try: diff --git a/werewolf/role.py b/werewolf/role.py index 0693f46..ccc20ae 100644 --- a/werewolf/role.py +++ b/werewolf/role.py @@ -129,7 +129,7 @@ class Role(WolfListener): return "Default" @wolflistener("at_game_start", priority=1) - async def _at_game_start(self, data=None): + async def _at_game_start(self): if self.channel_id: await self.game.register_channel(self.channel_id, self) diff --git a/werewolf/roles/vanillawerewolf.py b/werewolf/roles/vanillawerewolf.py index db70eb5..f6eea81 100644 --- a/werewolf/roles/vanillawerewolf.py +++ b/werewolf/roles/vanillawerewolf.py @@ -57,9 +57,9 @@ class VanillaWerewolf(Role): return "Werewolf" @wolflistener("at_game_start") - async def _at_game_start(self, data=None): + async def _at_game_start(self): if self.channel_id: - print("Wolf has channel_id: " + self.channel_id) + log.debug("Wolf has channel_id: " + self.channel_id) await self.game.register_channel( self.channel_id, self, WolfVote ) # Add VoteGroup WolfVote diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index a011ccb..742a890 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -47,7 +47,7 @@ class Werewolf(Cog): return def __unload(self): - print("Unload called") + log.debug("Unload called") for game in self.games.values(): del game @@ -182,7 +182,7 @@ class Werewolf(Cog): Joins a game of Werewolf """ - game = await self._get_game(ctx) + game: Game = await self._get_game(ctx) if not game: await ctx.send("No game to join!\nCreate a new one with `[p]ww new`") From 8ffc8cc707b7c3fd666949b4b94584a36b95cda5 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 24 Sep 2020 17:03:39 -0400 Subject: [PATCH 16/43] Missed the listener update --- werewolf/votegroups/wolfvote.py | 79 +++++---------------------------- 1 file changed, 11 insertions(+), 68 deletions(-) diff --git a/werewolf/votegroups/wolfvote.py b/werewolf/votegroups/wolfvote.py index 0823d77..7f6bbde 100644 --- a/werewolf/votegroups/wolfvote.py +++ b/werewolf/votegroups/wolfvote.py @@ -1,6 +1,9 @@ import logging import random +import discord + +from werewolf.listener import wolflistener from werewolf.votegroup import VoteGroup log = logging.getLogger("red.fox_v3.werewolf.votegroup.wolfvote") @@ -21,59 +24,13 @@ class WolfVote(VoteGroup): def __init__(self, game, channel): super().__init__(game, channel) - # self.game = game - # self.channel = channel - # self.players = [] - # self.vote_results = {} - # self.properties = {} # Extra data for other options self.killer = None # Added killer - self.action_list = [ - (self._at_game_start, 1), # (Action, Priority) - (self._at_day_start, 0), - (self._at_voted, 0), - (self._at_kill, 1), - (self._at_hang, 1), - (self._at_day_end, 0), - (self._at_night_start, 2), - (self._at_night_end, 5), # Kill priority - (self._at_visit, 0), - ] - - # async def on_event(self, event, data): - - # """ - # See Game class for event guide - # """ - # - # await action_list[event][0](data) - # - # async def _at_game_start(self, data=None): - # await self.channel.send(" ".join(player.mention for player in self.players)) - # - # async def _at_day_start(self, data=None): - # pass - # - # async def _at_voted(self, data=None): - # pass - # - # async def _at_kill(self, data=None): - # if data["player"] in self.players: - # self.players.pop(data["player"]) - # - # async def _at_hang(self, data=None): - # if data["player"] in self.players: - # self.players.pop(data["player"]) - # - # async def _at_day_end(self, data=None): - # pass - - async def _at_night_start(self, data=None): - if self.channel is None: - return + @wolflistener("at_night_start", priority=2) + async def _at_night_start(self): + await super()._at_night_start() - await self.game.generate_targets(self.channel) mention_list = " ".join(player.mention for player in self.players) if mention_list != "": await self.channel.send(mention_list) @@ -83,7 +40,8 @@ class WolfVote(VoteGroup): f"{self.killer.member.display_name} has been selected as tonight's killer" ) - async def _at_night_end(self, data=None): + @wolflistener("at_night_end", priority=5) + async def _at_night_end(self): if self.channel is None: return @@ -102,29 +60,14 @@ class WolfVote(VoteGroup): else: await self.channel.send("**No kill will be attempted tonight...**") - # async def _at_visit(self, data=None): - # pass - # - # async def register_players(self, *players): - # """ - # Extend players by passed list - # """ - # self.players.extend(players) - # - # async def remove_player(self, player): - # """ - # Remove a player from player list - # """ - # if player.id in self.players: - # self.players.remove(player) - async def vote(self, target, author, target_id): """ Receive vote from game """ - self.vote_results[author.id] = target_id + await super().vote(target, author, target_id) await self.channel.send( - "{} has voted to kill {}".format(author.mention, target.member.display_name) + "{} has voted to kill {}".format(author.mention, target.member.display_name), + allowed_mentions=discord.AllowedMentions(everyone=False, users=[author]) ) From eaa3e0a2f792d1f4dba535f60c4aa9c7475ea5f7 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 24 Sep 2020 17:04:11 -0400 Subject: [PATCH 17/43] Fix listener parameters and priority --- werewolf/votegroup.py | 57 +++++++++++++++---------------------------- 1 file changed, 20 insertions(+), 37 deletions(-) diff --git a/werewolf/votegroup.py b/werewolf/votegroup.py index 19ebd9e..2f0b3a0 100644 --- a/werewolf/votegroup.py +++ b/werewolf/votegroup.py @@ -22,49 +22,31 @@ class VoteGroup(WolfListener): self.vote_results = {} self.properties = {} # Extra data for other options - # self.action_list = [ - # (self._at_game_start, 1), # (Action, Priority) - # (self._at_day_start, 0), - # (self._at_voted, 0), - # (self._at_kill, 1), - # (self._at_hang, 1), - # (self._at_day_end, 0), - # (self._at_night_start, 2), - # (self._at_night_end, 0), - # (self._at_visit, 0), - # ] - - # async def on_event(self, event, data): - # """ - # See Game class for event guide - # """ - # - # await self.action_list[event][0](data) - - @wolflistener("at_game_start") - async def _at_game_start(self, data=None): + @wolflistener("at_game_start", priority=1) + async def _at_game_start(self): await self.channel.send(" ".join(player.mention for player in self.players)) - @wolflistener("at_kill") - async def _at_kill(self, data=None): - if data["player"] in self.players: - self.players.remove(data["player"]) + @wolflistener("at_kill", priority=1) + async def _at_kill(self, player): + if player in self.players: + self.players.remove(player) - # Removed, only if they actually die - # @wolflistener("at_hang") - # async def _at_hang(self, data=None): - # if data["player"] in self.players: - # self.players.remove(data["player"]) + @wolflistener("at_hang", priority=1) + async def _at_hang(self, player): + if player in self.players: + self.players.remove(player) - @wolflistener("at_night_start") - async def _at_night_start(self, data=None): + @wolflistener("at_night_start", priority=2) + async def _at_night_start(self): if self.channel is None: return + self.vote_results = {} + await self.game.generate_targets(self.channel) - @wolflistener("at_night_end") - async def _at_night_end(self, data=None): + @wolflistener("at_night_end", priority=5) + async def _at_night_end(self): if self.channel is None: return @@ -75,8 +57,8 @@ class VoteGroup(WolfListener): target = max(set(vote_list), key=vote_list.count) if target: - # Do what you voted on - pass + # Do what the votegroup votes on + raise NotImplementedError async def register_players(self, *players): """ @@ -92,7 +74,8 @@ class VoteGroup(WolfListener): self.players.remove(player) if not self.players: - # ToDo: Trigger deletion of votegroup + # TODO: Confirm deletion + self.game.to_delete.add(self) pass async def vote(self, target, author, target_id): From 39801aada912b3ac4bf61226f7207f8518dbdac2 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 24 Sep 2020 17:04:25 -0400 Subject: [PATCH 18/43] Missed priority --- werewolf/roles/vanillawerewolf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/werewolf/roles/vanillawerewolf.py b/werewolf/roles/vanillawerewolf.py index f6eea81..58b474e 100644 --- a/werewolf/roles/vanillawerewolf.py +++ b/werewolf/roles/vanillawerewolf.py @@ -56,7 +56,7 @@ class VanillaWerewolf(Role): """ return "Werewolf" - @wolflistener("at_game_start") + @wolflistener("at_game_start", priority=1) async def _at_game_start(self): if self.channel_id: log.debug("Wolf has channel_id: " + self.channel_id) From cb0a7f1041a76202a1622d61c7af77a592b667d7 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 24 Sep 2020 17:04:49 -0400 Subject: [PATCH 19/43] Add priority and parameters --- werewolf/roles/seer.py | 4 ++-- werewolf/roles/shifter.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/werewolf/roles/seer.py b/werewolf/roles/seer.py index f01c5c2..56624c9 100644 --- a/werewolf/roles/seer.py +++ b/werewolf/roles/seer.py @@ -65,7 +65,7 @@ class Seer(Role): return "Villager" @wolflistener("at_night_start", priority=2) - async def _at_night_start(self, data=None): + async def _at_night_start(self): if not self.player.alive: return self.see_target = None @@ -73,7 +73,7 @@ class Seer(Role): await self.player.send_dm("**Pick a target to see tonight**") @wolflistener("at_night_end", priority=4) - async def _at_night_end(self, data=None): + async def _at_night_end(self): if self.see_target is None: if self.player.alive: await self.player.send_dm("You will not use your powers tonight...") diff --git a/werewolf/roles/shifter.py b/werewolf/roles/shifter.py index 8f93d76..a7ea058 100644 --- a/werewolf/roles/shifter.py +++ b/werewolf/roles/shifter.py @@ -99,14 +99,14 @@ class Shifter(Role): """ return "Shifter" - @wolflistener("at_night_start") - async def _at_night_start(self, data=None): + @wolflistener("at_night_start", priority=2) + async def _at_night_start(self): self.shift_target = None await self.game.generate_targets(self.player.member) await self.player.send_dm("**Pick a target to shift into**") - @wolflistener("at_night_end") - async def _at_night_end(self, data=None): + @wolflistener("at_night_end", priority=6) + async def _at_night_end(self): if self.shift_target is None: if self.player.alive: await self.player.send_dm("You will not use your powers tonight...") From 61d131341154401c659e5f717b5b4af8a201e3fd Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 24 Sep 2020 17:05:52 -0400 Subject: [PATCH 20/43] Switch game to handle daytime smoother allowing cancellation --- werewolf/game.py | 121 +++++++++++++++++++++++++---------------------- 1 file changed, 65 insertions(+), 56 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index 45f0cbd..dd95edb 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -65,7 +65,7 @@ class Game: self.started = False self.game_over = False - self.can_vote = False + self.any_votes_remaining = False self.used_votes = 0 self.day_time = False @@ -88,6 +88,7 @@ class Game: self.loop = asyncio.get_event_loop() self.action_queue = deque() + self.current_action = None self.listeners = {} # def __del__(self): @@ -278,8 +279,12 @@ class Game: self.action_queue.append(self._at_day_start()) - while self.action_queue: - await self.action_queue.popleft() + while self.action_queue and not self.game_over: + current_action = asyncio.create_task(self.action_queue.popleft()) + try: + await current_action + except asyncio.CancelledError: + log.debug("Cancelled task") # # await self._at_day_start() # # Once cycle ends, this will trigger end_game @@ -299,52 +304,57 @@ class Game: if self.game_over: return + self.action_queue.append(self._at_day_end()) # Get this ready in case day is cancelled + def check(): - return not self.can_vote or not self.day_time or self.game_over + return not self.any_votes_remaining or not self.day_time or self.game_over self.day_count += 1 + + # Print the results of who died during the night embed = discord.Embed(title=random.choice(self.morning_messages).format(self.day_count)) for result in self.night_results: embed.add_field(name=result, value="________", inline=False) - self.day_time = True + self.day_time = True # True while day self.night_results = [] # Clear for next day await self.village_channel.send(embed=embed) - await self.generate_targets(self.village_channel) + await self.generate_targets(self.village_channel) # Print remaining players for voting await self.day_perms(self.village_channel) - await self._notify("at_day_start") + await self._notify("at_day_start") # Wait for day_start actions await self._check_game_over() - if self.game_over: + if self.game_over: # If game ended because of _notify return - self.can_vote = True + self.any_votes_remaining = True + + # Now we sleep and let the day happen. Print the remaining daylight half way through await asyncio.sleep(HALF_DAY_LENGTH) # 4 minute days FixMe to 120 later if check(): return await self.village_channel.send( - embed=discord.Embed(title="**Two minutes of daylight remain...**") + embed=discord.Embed(title=f"**{HALF_DAY_LENGTH/60} minutes of daylight remain...**") ) await asyncio.sleep(HALF_DAY_LENGTH) # 4 minute days FixMe to 120 later - # Need a loop here to wait for trial to end (can_vote?) + # Need a loop here to wait for trial to end while self.ongoing_vote: await asyncio.sleep(5) - if check(): - return - - self.action_queue.append(self._at_day_end()) + # Abruptly ends, assuming _day_end is next in queue async def _at_voted(self, target): # ID 2 if self.game_over: return - data = {"player": target} + + # Notify that a target has been chosen await self._notify("at_voted", player=target) + # TODO: Support pre-vote target modifying roles self.ongoing_vote = True self.used_votes += 1 @@ -359,7 +369,7 @@ class Game: await self.speech_perms(self.village_channel, target.member, undo=True) # No one can talk - message: discord.Message = await self.village_channel.send( + vote_message: discord.Message = await self.village_channel.send( f"Everyone will now vote whether to lynch {target.mention}\n" "👍 to save, 👎 to lynch\n" "*Majority rules, no-lynch on ties, " @@ -367,41 +377,47 @@ class Game: allowed_mentions=discord.AllowedMentions(everyone=False, users=[target]), ) - await message.add_reaction("👍") - await message.add_reaction("👎") + await vote_message.add_reaction("👍") + await vote_message.add_reaction("👎") await asyncio.sleep(15) - reaction_list = message.reactions + reaction_list = vote_message.reactions + + if True: # TODO: Allow customizable vote history deletion. + await vote_message.delete() - up_votes = sum(p for p in reaction_list if p.emoji == "👍" and not p.me) - down_votes = sum(p for p in reaction_list if p.emoji == "👎" and not p.me) + raw_up_votes = sum(p for p in reaction_list if p.emoji == "👍" and not p.me) + raw_down_votes = sum(p for p in reaction_list if p.emoji == "👎" and not p.me) - if down_votes > up_votes: - embed = discord.Embed(title="Vote Results", color=0xFF0000) + # TODO: Support vote count modifying roles. (Need notify and count function) + voted_to_lynch = raw_down_votes > raw_up_votes + + if voted_to_lynch: + embed = discord.Embed( + title="Vote Results", + description=f"**Voted to lynch {target.mention}!**", + color=0xFF0000, + ) else: - embed = discord.Embed(title="Vote Results", color=0x80FF80) + embed = discord.Embed( + title="Vote Results", + description=f"**{target.mention} has been spared!**", + color=0x80FF80, + ) - embed.add_field(name="👎", value=f"**{up_votes}**", inline=True) - embed.add_field(name="👍", value=f"**{down_votes}**", inline=True) + embed.add_field(name="👎", value=f"**{raw_up_votes}**", inline=True) + embed.add_field(name="👍", value=f"**{raw_down_votes}**", inline=True) await self.village_channel.send(embed=embed) - if down_votes > up_votes: - await self.village_channel.send( - f"**Voted to lynch {target.mention}!**", - allowed_mentions=discord.AllowedMentions(everyone=False, users=[target]), - ) + if voted_to_lynch: await self.lynch(target) - self.can_vote = False + self.any_votes_remaining = False else: - await self.village_channel.send( - f"**{target.mention} has been spared!**", - allowed_mentions=discord.AllowedMentions(everyone=False, users=[target]), - ) if self.used_votes >= self.day_vote_count: await self.village_channel.send("**All votes have been used! Day is now over!**") - self.can_vote = False + self.any_votes_remaining = False else: await self.village_channel.send( f"**{self.used_votes}**/**{self.day_vote_count}** of today's votes have been used!\n" @@ -410,21 +426,19 @@ class Game: self.ongoing_vote = False - if not self.can_vote: - self.action_queue.append(self._at_day_end()) + if not self.any_votes_remaining and self.day_time: + self.current_action.cancel() else: await self.normal_perms(self.village_channel) # No point if about to be night async def _at_kill(self, target): # ID 3 if self.game_over: return - data = {"player": target} await self._notify("at_kill", player=target) async def _at_hang(self, target): # ID 4 if self.game_over: return - data = {"player": target} await self._notify("at_hang", player=target) async def _at_day_end(self): # ID 5 @@ -433,7 +447,7 @@ class Game: if self.game_over: return - self.can_vote = False + self.any_votes_remaining = False self.day_vote = {} self.vote_totals = {} self.day_time = False @@ -476,7 +490,6 @@ class Game: async def _at_visit(self, target, source): # ID 8 if self.game_over: return - data = {"target": target, "source": source} await self._notify("at_visit", target=target, source=source) async def _notify(self, event, **kwargs): @@ -484,6 +497,8 @@ class Game: tasks = [] for event in self.listeners.get(event, {}).get(i, []): tasks.append(asyncio.ensure_future(event(**kwargs), loop=self.loop)) + + # Run same-priority task simultaneously await asyncio.gather(*tasks) # self.bot.dispatch(f"red.fox.werewolf.{event}", data=data, priority=i) @@ -507,8 +522,7 @@ class Game: async def generate_targets(self, channel, with_roles=False): embed = discord.Embed(title="Remaining Players") - for i in range(len(self.players)): - player = self.players[i] + for i, player in enumerate(self.players): if player.alive: status = "" else: @@ -653,7 +667,7 @@ class Game: return if channel == self.village_channel: - if not self.can_vote: + if not self.any_votes_remaining: await channel.send("Voting is not allowed right now") return elif channel.name in self.p_channels: @@ -695,13 +709,8 @@ class Game: if self.vote_totals[target_id] < required_votes: await self.village_channel.send( - "" - "{} has voted to put {} to trial. " - "{} more votes needed".format( - author.mention, - target.member.mention, - required_votes - self.vote_totals[target_id], - ), + f"{author.mention} has voted to put {target.member.mention} to trial. " + f"{required_votes - self.vote_totals[target_id]} more votes needed", allowed_mentions=discord.AllowedMentions(everyone=False, users=[author, target]), ) else: @@ -771,8 +780,8 @@ class Game: Attempt to lynch a target Important to finish execution before triggering notify """ - target = await self.get_day_target(target_id) - target.alive = False + target = await self.get_day_target(target_id) # Allows target modification + target.alive = False # Kill them, await self._at_hang(target) if not target.alive: # Still dead after notifying await self.dead_perms(self.village_channel, target.member) From f263f97cc27bef8af33cd5998fd632a97db38845 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 24 Sep 2020 17:06:03 -0400 Subject: [PATCH 21/43] Update builder to accept any number of roles --- werewolf/builder.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/werewolf/builder.py b/werewolf/builder.py index ca90eca..62d290f 100644 --- a/werewolf/builder.py +++ b/werewolf/builder.py @@ -9,21 +9,33 @@ import discord # Import all roles here from redbot.core import commands -from .roles.seer import Seer -from .roles.vanillawerewolf import VanillaWerewolf -from .roles.villager import Villager +# from .roles.seer import Seer +# from .roles.vanillawerewolf import VanillaWerewolf +# from .roles.villager import Villager + +from werewolf import roles from redbot.core.utils.menus import menu, prev_page, next_page, close_menu +from werewolf.role import Role + log = logging.getLogger("red.fox_v3.werewolf.builder") # All roles in this list for iterating -ROLE_LIST = sorted([Villager, Seer, VanillaWerewolf], key=lambda x: x.alignment) +ROLE_DICT = {name: cls for name, cls in roles.__dict__.items() if isinstance(cls, type)} +ROLE_LIST = sorted( + [cls for cls in ROLE_DICT.values()], + key=lambda x: x.alignment, +) + +log.debug(f"{ROLE_DICT=}") ALIGNMENT_COLORS = [0x008000, 0xFF0000, 0xC0C0C0] -TOWN_ROLES = [(idx, role) for idx, role in enumerate(ROLE_LIST) if role.alignment == 1] -WW_ROLES = [(idx, role) for idx, role in enumerate(ROLE_LIST) if role.alignment == 2] -OTHER_ROLES = [(idx, role) for idx, role in enumerate(ROLE_LIST) if role.alignment not in [0, 1]] +# TOWN_ROLES = [(idx, role) for idx, r_tuple in enumerate(ROLE_LIST) if role.alignment == 1] +# WW_ROLES = [(idx, role) for idx, r_tuple in enumerate(ROLE_LIST) if role.alignment == 2] +# OTHER_ROLES = [ +# (idx, role) for idx, r_tuple in enumerate(ROLE_LIST) if role.alignment not in [0, 1] +# ] ROLE_PAGES = [] PAGE_GROUPS = [0] @@ -81,9 +93,7 @@ def setup(): for k, v in ROLE_CATEGORIES.items(): if 0 < k <= 6: ROLE_PAGES.append( - discord.Embed( - title="RANDOM:Town Role", description=f"Town {v}", color=0x008000 - ) + discord.Embed(title="RANDOM:Town Role", description=f"Town {v}", color=0x008000) ) CATEGORY_COUNT.append(k) From 224ff93531179c1ccc73dbbd060172ca4e26b7dd Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 24 Sep 2020 17:06:17 -0400 Subject: [PATCH 22/43] black and __all__ --- werewolf/roles/__init__.py | 1 + werewolf/votegroups/__init__.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/werewolf/roles/__init__.py b/werewolf/roles/__init__.py index ba929e5..201799a 100644 --- a/werewolf/roles/__init__.py +++ b/werewolf/roles/__init__.py @@ -3,3 +3,4 @@ from .shifter import Shifter from .vanillawerewolf import VanillaWerewolf from .villager import Villager +__all__ = ["Seer", "Shifter", "VanillaWerewolf", "Villager"] diff --git a/werewolf/votegroups/__init__.py b/werewolf/votegroups/__init__.py index 03abc1b..6b99b1e 100644 --- a/werewolf/votegroups/__init__.py +++ b/werewolf/votegroups/__init__.py @@ -1 +1 @@ -from .wolfvote import WolfVote \ No newline at end of file +from .wolfvote import WolfVote From a0c645bd28f9aa07029776e7b1c99baae4f6919d Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 25 Sep 2020 15:56:33 -0400 Subject: [PATCH 23/43] Precarious import order --- werewolf/roles/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/werewolf/roles/__init__.py b/werewolf/roles/__init__.py index 201799a..3f58a76 100644 --- a/werewolf/roles/__init__.py +++ b/werewolf/roles/__init__.py @@ -1,6 +1,11 @@ +from .villager import Villager from .seer import Seer -from .shifter import Shifter + from .vanillawerewolf import VanillaWerewolf -from .villager import Villager + +from .shifter import Shifter + +# Don't sort these imports. They are unstably in order +# TODO: Replace with unique IDs for roles in the future __all__ = ["Seer", "Shifter", "VanillaWerewolf", "Villager"] From eb0c79ef1d57924c650c96efc90998fe326d4d01 Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 25 Sep 2020 15:56:49 -0400 Subject: [PATCH 24/43] Introduction of The Blob --- werewolf/roles/blob.py | 101 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 werewolf/roles/blob.py diff --git a/werewolf/roles/blob.py b/werewolf/roles/blob.py new file mode 100644 index 0000000..bd7b598 --- /dev/null +++ b/werewolf/roles/blob.py @@ -0,0 +1,101 @@ +import logging +import random + +from werewolf.constants import ALIGNMENT_NEUTRAL, CATEGORY_NEUTRAL_EVIL +from werewolf.listener import wolflistener +from werewolf.player import Player +from werewolf.role import Role + +log = logging.getLogger("red.fox_v3.werewolf.role.blob") + + +class TheBlob(Role): + rand_choice = True + category = [CATEGORY_NEUTRAL_EVIL] # List of enrolled categories + alignment = ALIGNMENT_NEUTRAL # 1: Town, 2: Werewolf, 3: Neutral + channel_id = "" # Empty for no private channel + unique = True # Only one of this role per game + game_start_message = ( + "Your role is **The Blob**\n" + "You win by absorbing everyone town\n" + "Lynch players during the day with `[p]ww vote `\n" + "Each night you will absorb an adjacent player" + ) + description = ( + "A mysterious green blob of jelly, slowly growing in size.\n" + "The Blob fears no evil, must be dealt with in town" + ) + + def __init__(self, game): + super().__init__(game) + + self.blob_target = None + + async def see_alignment(self, source=None): + """ + Interaction for investigative roles attempting + to see team (Village, Werewolf, Other) + """ + return ALIGNMENT_NEUTRAL + + async def get_role(self, source=None): + """ + Interaction for powerful access of role + Unlikely to be able to deceive this + """ + return "The Blob" + + async def see_role(self, source=None): + """ + Interaction for investigative roles. + More common to be able to deceive these roles + """ + return "The Blob" + + async def kill(self, source): + """ + Called when someone is trying to kill you! + Can you do anything about it? + self.player.alive is now set to False, set to True to stay alive + """ + + # Blob cannot simply be killed + self.player.alive = True + + @wolflistener("at_night_start", priority=2) + async def _at_night_start(self): + if not self.player.alive: + return + + self.blob_target = None + idx = self.player.id + left_or_right = random.choice((-1, 1)) + while self.blob_target is None: + idx += left_or_right + if idx >= len(self.game.players): + idx = 0 + + player = self.game.players[idx] + + # you went full circle, everyone is a blob or something else is wrong + if player == self.player: + break + + if player.role.properties.get("been_blobbed", False): + self.blob_target = player + + if self.blob_target is not None: + await self.player.send_dm(f"**You will attempt to absorb {self.blob_target} tonight**") + else: + await self.player.send_dm(f"**No player will be absorbed tonight**") + + @wolflistener("at_night_end", priority=4) + async def _at_night_end(self): + if self.blob_target is None or not self.player.alive: + return + + target: "Player" = await self.game.visit(self.blob_target, self.player) + + if target is not None: + target.role.properties["been_blobbed"] = True + self.game.night_messages.append("The Blob grows...") From 61049c23432cd8262e5f1805a30cdae7cc2af569 Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 25 Sep 2020 15:59:21 -0400 Subject: [PATCH 25/43] Adding constants --- werewolf/builder.py | 258 +++++++++++++++--------------- werewolf/constants.py | 91 +++++++++++ werewolf/game.py | 10 +- werewolf/role.py | 59 ++----- werewolf/roles/seer.py | 23 +-- werewolf/roles/shifter.py | 8 +- werewolf/roles/vanillawerewolf.py | 20 +-- werewolf/roles/villager.py | 16 +- werewolf/werewolf.py | 2 +- 9 files changed, 273 insertions(+), 214 deletions(-) create mode 100644 werewolf/constants.py diff --git a/werewolf/builder.py b/werewolf/builder.py index 62d290f..4c803cc 100644 --- a/werewolf/builder.py +++ b/werewolf/builder.py @@ -1,6 +1,7 @@ import bisect import logging from collections import defaultdict +from operator import attrgetter from random import choice import discord @@ -16,6 +17,7 @@ from redbot.core import commands from werewolf import roles from redbot.core.utils.menus import menu, prev_page, next_page, close_menu +from werewolf.constants import ROLE_CATEGORY_DESCRIPTIONS from werewolf.role import Role log = logging.getLogger("red.fox_v3.werewolf.builder") @@ -25,104 +27,40 @@ log = logging.getLogger("red.fox_v3.werewolf.builder") ROLE_DICT = {name: cls for name, cls in roles.__dict__.items() if isinstance(cls, type)} ROLE_LIST = sorted( [cls for cls in ROLE_DICT.values()], - key=lambda x: x.alignment, + key=attrgetter('alignment'), ) log.debug(f"{ROLE_DICT=}") +# Town, Werewolf, Neutral ALIGNMENT_COLORS = [0x008000, 0xFF0000, 0xC0C0C0] -# TOWN_ROLES = [(idx, role) for idx, r_tuple in enumerate(ROLE_LIST) if role.alignment == 1] -# WW_ROLES = [(idx, role) for idx, r_tuple in enumerate(ROLE_LIST) if role.alignment == 2] -# OTHER_ROLES = [ -# (idx, role) for idx, r_tuple in enumerate(ROLE_LIST) if role.alignment not in [0, 1] -# ] ROLE_PAGES = [] -PAGE_GROUPS = [0] - -ROLE_CATEGORIES = { - 1: "Random", - 2: "Investigative", - 3: "Protective", - 4: "Government", - 5: "Killing", - 6: "Power (Special night action)", - 11: "Random", - 12: "Deception", - 15: "Killing", - 16: "Support", - 21: "Benign", - 22: "Evil", - 23: "Killing", -} - -CATEGORY_COUNT = [] - - -def role_embed(idx, role, color): + + +def role_embed(idx, role: Role, color): embed = discord.Embed( title=f"**{idx}** - {role.__name__}", description=role.game_start_message, color=color, ) + if role.icon_url is not None: + embed.set_thumbnail(url=role.icon_url) + embed.add_field( - name="Alignment", value=["Town", "Werewolf", "Neutral"][role.alignment - 1], inline=True + name="Alignment", value=["Town", "Werewolf", "Neutral"][role.alignment - 1], inline=False ) - embed.add_field(name="Multiples Allowed", value=str(not role.unique), inline=True) + embed.add_field(name="Multiples Allowed", value=str(not role.unique), inline=False) embed.add_field( - name="Role Type", value=", ".join(ROLE_CATEGORIES[x] for x in role.category), inline=True + name="Role Types", + value=", ".join(ROLE_CATEGORY_DESCRIPTIONS[x] for x in role.category), + inline=False, ) - embed.add_field(name="Random Option", value=str(role.rand_choice), inline=True) + embed.add_field(name="Random Option", value=str(role.rand_choice), inline=False) return embed -def setup(): - # Roles - last_alignment = ROLE_LIST[0].alignment - for idx, role in enumerate(ROLE_LIST): - if role.alignment != last_alignment and len(ROLE_PAGES) - 1 not in PAGE_GROUPS: - PAGE_GROUPS.append(len(ROLE_PAGES) - 1) - last_alignment = role.alignment - - ROLE_PAGES.append(role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1])) - - # Random Town Roles - if len(ROLE_PAGES) - 1 not in PAGE_GROUPS: - PAGE_GROUPS.append(len(ROLE_PAGES) - 1) - for k, v in ROLE_CATEGORIES.items(): - if 0 < k <= 6: - ROLE_PAGES.append( - discord.Embed(title="RANDOM:Town Role", description=f"Town {v}", color=0x008000) - ) - CATEGORY_COUNT.append(k) - - # Random WW Roles - if len(ROLE_PAGES) - 1 not in PAGE_GROUPS: - PAGE_GROUPS.append(len(ROLE_PAGES) - 1) - for k, v in ROLE_CATEGORIES.items(): - if 10 < k <= 16: - ROLE_PAGES.append( - discord.Embed( - title="RANDOM:Werewolf Role", - description=f"Werewolf {v}", - color=0xFF0000, - ) - ) - CATEGORY_COUNT.append(k) - # Random Neutral Roles - if len(ROLE_PAGES) - 1 not in PAGE_GROUPS: - PAGE_GROUPS.append(len(ROLE_PAGES) - 1) - for k, v in ROLE_CATEGORIES.items(): - if 20 < k <= 26: - ROLE_PAGES.append( - discord.Embed( - title=f"RANDOM:Neutral Role", description="Neutral {v}", color=0xC0C0C0 - ) - ) - CATEGORY_COUNT.append(k) - - """ Example code: 0 = Villager @@ -189,15 +127,15 @@ async def parse_code(code, game): return decode -async def encode(roles, rand_roles): +async def encode(role_list, rand_roles): """Convert role list to code""" out_code = "" - digit_sort = sorted(role for role in roles if role < 10) + digit_sort = sorted(role for role in role_list if role < 10) for role in digit_sort: out_code += str(role) - digit_sort = sorted(role for role in roles if 10 <= role < 100) + digit_sort = sorted(role for role in role_list if 10 <= role < 100) if digit_sort: out_code += "-" for role in digit_sort: @@ -229,51 +167,6 @@ async def encode(roles, rand_roles): return out_code -async def next_group( - ctx: commands.Context, - pages: list, - controls: dict, - message: discord.Message, - page: int, - timeout: float, - emoji: str, -): - perms = message.channel.permissions_for(ctx.me) - if perms.manage_messages: # Can manage messages, so remove react - try: - await message.remove_reaction(emoji, ctx.author) - except discord.NotFound: - pass - page = bisect.bisect_right(PAGE_GROUPS, page) - - if page == len(PAGE_GROUPS): - page = PAGE_GROUPS[0] - else: - page = PAGE_GROUPS[page] - - return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout) - - -async def prev_group( - ctx: commands.Context, - pages: list, - controls: dict, - message: discord.Message, - page: int, - timeout: float, - emoji: str, -): - perms = message.channel.permissions_for(ctx.me) - if perms.manage_messages: # Can manage messages, so remove react - try: - await message.remove_reaction(emoji, ctx.author) - except discord.NotFound: - pass - page = PAGE_GROUPS[bisect.bisect_left(PAGE_GROUPS, page) - 1] - - return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout) - - def role_from_alignment(alignment): return [ role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]) @@ -316,11 +209,11 @@ def say_role_list(code_list, rand_roles): for role in rand_roles: if 0 < role <= 6: - role_dict[f"Town {ROLE_CATEGORIES[role]}"] += 1 + role_dict[f"Town {ROLE_CATEGORY_DESCRIPTIONS[role]}"] += 1 if 10 < role <= 16: - role_dict[f"Werewolf {ROLE_CATEGORIES[role]}"] += 1 + role_dict[f"Werewolf {ROLE_CATEGORY_DESCRIPTIONS[role]}"] += 1 if 20 < role <= 26: - role_dict[f"Neutral {ROLE_CATEGORIES[role]}"] += 1 + role_dict[f"Neutral {ROLE_CATEGORY_DESCRIPTIONS[role]}"] += 1 for k, v in role_dict.items(): embed.add_field(name=k, value=f"Count: {v}", inline=True) @@ -332,15 +225,69 @@ class GameBuilder: def __init__(self): self.code = [] self.rand_roles = [] - setup() + self.page_groups = [0] + self.category_count = [] + + self.setup() + + def setup(self): + # Roles + last_alignment = ROLE_LIST[0].alignment + for idx, role in enumerate(ROLE_LIST): + if role.alignment != last_alignment and len(ROLE_PAGES) - 1 not in self.page_groups: + self.page_groups.append(len(ROLE_PAGES) - 1) + last_alignment = role.alignment + + ROLE_PAGES.append(role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1])) + + # Random Town Roles + if len(ROLE_PAGES) - 1 not in self.page_groups: + self.page_groups.append(len(ROLE_PAGES) - 1) + for k, v in ROLE_CATEGORY_DESCRIPTIONS.items(): + if 0 < k <= 9: + ROLE_PAGES.append( + discord.Embed( + title="RANDOM:Town Role", + description=f"Town {v}", + color=ALIGNMENT_COLORS[0], + ) + ) + self.category_count.append(k) + + # Random WW Roles + if len(ROLE_PAGES) - 1 not in self.page_groups: + self.page_groups.append(len(ROLE_PAGES) - 1) + for k, v in ROLE_CATEGORY_DESCRIPTIONS.items(): + if 10 < k <= 19: + ROLE_PAGES.append( + discord.Embed( + title="RANDOM:Werewolf Role", + description=f"Werewolf {v}", + color=ALIGNMENT_COLORS[1], + ) + ) + self.category_count.append(k) + # Random Neutral Roles + if len(ROLE_PAGES) - 1 not in self.page_groups: + self.page_groups.append(len(ROLE_PAGES) - 1) + for k, v in ROLE_CATEGORY_DESCRIPTIONS.items(): + if 20 < k <= 29: + ROLE_PAGES.append( + discord.Embed( + title=f"RANDOM:Neutral Role", + description=f"Neutral {v}", + color=ALIGNMENT_COLORS[2], + ) + ) + self.category_count.append(k) async def build_game(self, ctx: commands.Context): new_controls = { - "⏪": prev_group, + "⏪": self.prev_group, "⬅": prev_page, "☑": self.select_page, "➡": next_page, - "⏩": next_group, + "⏩": self.next_group, "📇": self.list_roles, "❌": close_menu, } @@ -391,8 +338,53 @@ class GameBuilder: pass if page >= len(ROLE_LIST): - self.rand_roles.append(CATEGORY_COUNT[page - len(ROLE_LIST)]) + self.rand_roles.append(self.category_count[page - len(ROLE_LIST)]) else: self.code.append(page) return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout) + + async def next_group( + self, + ctx: commands.Context, + pages: list, + controls: dict, + message: discord.Message, + page: int, + timeout: float, + emoji: str, + ): + perms = message.channel.permissions_for(ctx.me) + if perms.manage_messages: # Can manage messages, so remove react + try: + await message.remove_reaction(emoji, ctx.author) + except discord.NotFound: + pass + page = bisect.bisect_right(self.page_groups, page) + + if page == len(self.page_groups): + page = self.page_groups[0] + else: + page = self.page_groups[page] + + return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout) + + async def prev_group( + self, + ctx: commands.Context, + pages: list, + controls: dict, + message: discord.Message, + page: int, + timeout: float, + emoji: str, + ): + perms = message.channel.permissions_for(ctx.me) + if perms.manage_messages: # Can manage messages, so remove react + try: + await message.remove_reaction(emoji, ctx.author) + except discord.NotFound: + pass + page = self.page_groups[bisect.bisect_left(self.page_groups, page) - 1] + + return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout) diff --git a/werewolf/constants.py b/werewolf/constants.py new file mode 100644 index 0000000..bb77421 --- /dev/null +++ b/werewolf/constants.py @@ -0,0 +1,91 @@ +""" +Role Constants + + Role Alignment guide as follows: + Town: 1 + Werewolf: 2 + Neutral: 3 + + Additional alignments may be added when warring factions are added + (Rival werewolves, cultists, vampires) + + Role Category enrollment guide as follows (See Role.category): + Town: + 1: Random, 2: Investigative, 3: Protective, 4: Government, + 5: Killing, 6: Power (Special night action) + + Werewolf: + 11: Random, 12: Deception, 15: Killing, 16: Support + + Neutral: + 21: Benign, 22: Evil, 23: Killing + + + Example category: + category = [1, 5, 6] Could be Veteran + category = [1, 5] Could be Bodyguard + category = [11, 16] Could be Werewolf Silencer + category = [22] Could be Blob (non-killing) + category = [22, 23] Could be Serial-Killer +""" + + +ALIGNMENT_TOWN = 1 +ALIGNMENT_WEREWOLF = 2 +ALIGNMENT_NEUTRAL = 3 +ALIGNMENT_MAP = {"Town": 1, "Werewolf": 2, "Neutral": 3} + +# 0-9: Town Role Categories +# 10-19: Werewolf Role Categories +# 20-29: Neutral Role Categories +CATEGORY_TOWN_RANDOM = 1 +CATEGORY_TOWN_INVESTIGATIVE = 2 +CATEGORY_TOWN_PROTECTIVE = 3 +CATEGORY_TOWN_GOVERNMENT = 4 +CATEGORY_TOWN_KILLING = 5 +CATEGORY_TOWN_POWER = 6 + +CATEGORY_WW_RANDOM = 11 +CATEGORY_WW_DECEPTION = 12 +CATEGORY_WW_KILLING = 15 +CATEGORY_WW_SUPPORT = 16 + +CATEGORY_NEUTRAL_BENIGN = 21 +CATEGORY_NEUTRAL_EVIL = 22 +CATEGORY_NEUTRAL_KILLING = 23 + +ROLE_CATEGORY_DESCRIPTIONS = { + CATEGORY_TOWN_RANDOM: "Random", + CATEGORY_TOWN_INVESTIGATIVE: "Investigative", + CATEGORY_TOWN_PROTECTIVE: "Protective", + CATEGORY_TOWN_GOVERNMENT: "Government", + CATEGORY_TOWN_KILLING: "Killing", + CATEGORY_TOWN_POWER: "Power (Special night action)", + CATEGORY_WW_RANDOM: "Random", + CATEGORY_WW_DECEPTION: "Deception", + CATEGORY_WW_KILLING: "Killing", + CATEGORY_WW_SUPPORT: "Support", + CATEGORY_NEUTRAL_BENIGN: "Benign", + CATEGORY_NEUTRAL_EVIL: "Evil", + CATEGORY_NEUTRAL_KILLING: "Killing", +} + + +""" +Listener Actions Priority Guide + + Action priority guide as follows (see listeners.py for wolflistener): + _at_night_start + 0. No Action + 1. Detain actions (Jailer/Kidnapper) + 2. Group discussions and choose targets + + _at_night_end + 0. No Action + 1. Self actions (Veteran) + 2. Target switching and role blocks (bus driver, witch, escort) + 3. Protection / Preempt actions (bodyguard/framer) + 4. Non-disruptive actions (seer/silencer) + 5. Disruptive actions (Killing) + 6. Role altering actions (Cult / Mason / Shifter) +""" diff --git a/werewolf/game.py b/werewolf/game.py index dd95edb..13db415 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -639,14 +639,14 @@ class Game: await target.role.visit(source) await self._at_visit(target, source) - async def visit(self, target_id, source): + async def visit(self, target_id, source) -> Union[Player, None]: """ Night visit target_id Returns a target for role information (i.e. Seer) """ if source.role.blocked: # Blocker handles text - return + return None target = await self.get_night_target(target_id, source) await self._visit(target, source) return target @@ -786,10 +786,10 @@ class Game: if not target.alive: # Still dead after notifying await self.dead_perms(self.village_channel, target.member) - async def get_night_target(self, target_id, source=None): + async def get_night_target(self, target_id, source=None) -> Player: return self.players[target_id] # ToDo check source - async def get_day_target(self, target_id, source=None): + async def get_day_target(self, target_id, source=None) -> Player: return self.players[target_id] # ToDo check source async def set_code(self, ctx: commands.Context, game_code): @@ -829,7 +829,7 @@ class Game: # Sorted players, now assign id's await self.players[idx].assign_id(idx) - async def get_player_by_member(self, member): + async def get_player_by_member(self, member: discord.Member): for player in self.players: if player.member == member: return player diff --git a/werewolf/role.py b/werewolf/role.py index ccc20ae..db7b852 100644 --- a/werewolf/role.py +++ b/werewolf/role.py @@ -26,6 +26,8 @@ class Role(WolfListener): category = [1, 5, 6] Could be Veteran category = [1, 5] Could be Bodyguard category = [11, 16] Could be Werewolf Silencer + category = [22] Could be Blob (non-killing) + category = [22, 23] Could be Serial-Killer Action priority guide as follows (on_event function): @@ -44,10 +46,12 @@ class Role(WolfListener): 6. Role altering actions (Cult / Mason / Shifter) """ - rand_choice = False # Determines if it can be picked as a random role (False for unusually disruptive roles) + # Determines if it can be picked as a random role (False for unusually disruptive roles) + rand_choice = False # TODO: Rework random with categories + town_balance = 0 # Guess at power level and it's balance on the town category = [0] # List of enrolled categories (listed above) alignment = 0 # 1: Town, 2: Werewolf, 3: Neutral - channel_id = "" # Empty for no private channel + channel_name = "" # Empty for no private channel unique = False # Only one of this role per game game_start_message = ( "Your role is **Default**\n" @@ -68,28 +72,9 @@ class Role(WolfListener): self.blocked = False self.properties = {} # Extra data for other roles (i.e. arsonist) - # 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), - # ] - def __repr__(self): return self.__class__.__name__ - # async def on_event(self, event, data): - # """ - # See Game class for event guide - # """ - # - # await self.action_list[event][0](data) - async def assign_player(self, player): """ Give this role a player @@ -110,7 +95,7 @@ class Role(WolfListener): async def see_alignment(self, source=None): """ Interaction for investigative roles attempting - to see alignment (Village, Werewolf Other) + to see alignment (Village, Werewolf, Other) """ return "Other" @@ -128,37 +113,13 @@ class Role(WolfListener): """ return "Default" - @wolflistener("at_game_start", priority=1) + @wolflistener("at_game_start", priority=2) async def _at_game_start(self): - if self.channel_id: - await self.game.register_channel(self.channel_id, self) + if self.channel_name: + await self.game.register_channel(self.channel_name, self) await self.player.send_dm(self.game_start_message) # Maybe embeds eventually - # async def _at_day_start(self, data=None): - # pass - # - # async def _at_voted(self, data=None): - # pass - # - # async def _at_kill(self, data=None): - # pass - # - # async def _at_hang(self, data=None): - # pass - # - # async def _at_day_end(self, data=None): - # pass - # - # async def _at_night_start(self, data=None): - # pass - # - # async def _at_night_end(self, data=None): - # pass - # - # async def _at_visit(self, data=None): - # pass - async def kill(self, source): """ Called when someone is trying to kill you! diff --git a/werewolf/roles/seer.py b/werewolf/roles/seer.py index 56624c9..32ace18 100644 --- a/werewolf/roles/seer.py +++ b/werewolf/roles/seer.py @@ -1,5 +1,7 @@ import logging +from werewolf.constants import ALIGNMENT_TOWN, ALIGNMENT_WEREWOLF, CATEGORY_TOWN_INVESTIGATIVE, \ + CATEGORY_TOWN_RANDOM from werewolf.listener import wolflistener from werewolf.night_powers import pick_target from werewolf.role import Role @@ -8,9 +10,12 @@ log = logging.getLogger("red.fox_v3.werewolf.role.seer") class Seer(Role): - rand_choice = True # Determines if it can be picked as a random role (False for unusually disruptive roles) - category = [1, 2] # List of enrolled categories (listed above) - alignment = 1 # 1: Town, 2: Werewolf, 3: Neutral + rand_choice = True + category = [ + CATEGORY_TOWN_RANDOM, + CATEGORY_TOWN_INVESTIGATIVE, + ] # List of enrolled categories (listed above) + alignment = ALIGNMENT_TOWN # 1: Town, 2: Werewolf, 3: Neutral channel_id = "" # Empty for no private channel unique = False # Only one of this role per game game_start_message = ( @@ -46,23 +51,23 @@ class Seer(Role): async def see_alignment(self, source=None): """ Interaction for investigative roles attempting - to see team (Village, Werewolf Other) + to see team (Village, Werewolf, Other) """ - return "Village" + return ALIGNMENT_TOWN async def get_role(self, source=None): """ Interaction for powerful access of role Unlikely to be able to deceive this """ - return "Villager" + return "Seer" async def see_role(self, source=None): """ Interaction for investigative roles. More common to be able to deceive these roles """ - return "Villager" + return "Seer" @wolflistener("at_night_start", priority=2) async def _at_night_start(self): @@ -84,9 +89,9 @@ class Seer(Role): if target: alignment = await target.role.see_alignment(self.player) - if alignment == "Werewolf": + if alignment == ALIGNMENT_WEREWOLF: out = "Your insight reveals this player to be a **Werewolf!**" - else: + else: # Don't reveal neutrals out = "You fail to find anything suspicious about this player..." await self.player.send_dm(out) diff --git a/werewolf/roles/shifter.py b/werewolf/roles/shifter.py index a7ea058..9685e20 100644 --- a/werewolf/roles/shifter.py +++ b/werewolf/roles/shifter.py @@ -1,5 +1,6 @@ import logging +from werewolf.constants import ALIGNMENT_NEUTRAL, CATEGORY_NEUTRAL_BENIGN from werewolf.listener import wolflistener from werewolf.night_powers import pick_target from werewolf.role import Role @@ -46,8 +47,9 @@ class Shifter(Role): """ rand_choice = False # Determines if it can be picked as a random role (False for unusually disruptive roles) - category = [22] # List of enrolled categories (listed above) - alignment = 3 # 1: Town, 2: Werewolf, 3: Neutral + town_balance = -3 + category = [CATEGORY_NEUTRAL_BENIGN] # List of enrolled categories (listed above) + alignment = ALIGNMENT_NEUTRAL # 1: Town, 2: Werewolf, 3: Neutral channel_id = "" # Empty for no private channel unique = False # Only one of this role per game game_start_message = ( @@ -81,7 +83,7 @@ class Shifter(Role): async def see_alignment(self, source=None): """ Interaction for investigative roles attempting - to see alignment (Village, Werewolf, Other) + to see alignment (Village, Werewolf,, Other) """ return "Other" diff --git a/werewolf/roles/vanillawerewolf.py b/werewolf/roles/vanillawerewolf.py index 58b474e..74e8d96 100644 --- a/werewolf/roles/vanillawerewolf.py +++ b/werewolf/roles/vanillawerewolf.py @@ -1,5 +1,6 @@ import logging +from werewolf.constants import ALIGNMENT_WEREWOLF, CATEGORY_WW_KILLING, CATEGORY_WW_RANDOM from werewolf.listener import wolflistener from werewolf.role import Role from werewolf.votegroups.wolfvote import WolfVote @@ -9,9 +10,10 @@ log = logging.getLogger("red.fox_v3.werewolf.role.vanillawerewolf") class VanillaWerewolf(Role): rand_choice = True - category = [11, 15] - alignment = 2 # 1: Town, 2: Werewolf, 3: Neutral - channel_id = "werewolves" + town_balance = -6 + category = [CATEGORY_WW_RANDOM, CATEGORY_WW_KILLING] + alignment = ALIGNMENT_WEREWOLF # 1: Town, 2: Werewolf, 3: Neutral + channel_name = "werewolves" unique = False game_start_message = ( "Your role is **Werewolf**\n" @@ -40,14 +42,14 @@ class VanillaWerewolf(Role): Interaction for investigative roles attempting to see team (Village, Werewolf Other) """ - return "Werewolf" + return ALIGNMENT_WEREWOLF async def get_role(self, source=None): """ Interaction for powerful access of role Unlikely to be able to deceive this """ - return "Werewolf" + return "VanillaWerewolf" async def see_role(self, source=None): """ @@ -56,12 +58,12 @@ class VanillaWerewolf(Role): """ return "Werewolf" - @wolflistener("at_game_start", priority=1) + @wolflistener("at_game_start", priority=2) async def _at_game_start(self): - if self.channel_id: - log.debug("Wolf has channel_id: " + self.channel_id) + if self.channel_name: + log.debug("Wolf has channel_name: " + self.channel_name) await self.game.register_channel( - self.channel_id, self, WolfVote + self.channel_name, self, WolfVote ) # Add VoteGroup WolfVote await self.player.send_dm(self.game_start_message) diff --git a/werewolf/roles/villager.py b/werewolf/roles/villager.py index f225e0d..d669ef9 100644 --- a/werewolf/roles/villager.py +++ b/werewolf/roles/villager.py @@ -1,14 +1,20 @@ import logging +from werewolf.constants import ALIGNMENT_TOWN, CATEGORY_TOWN_RANDOM from werewolf.role import Role log = logging.getLogger("red.fox_v3.werewolf.role.villager") class Villager(Role): - rand_choice = True # Determines if it can be picked as a random role (False for unusually disruptive roles) - category = [1] # List of enrolled categories (listed above) - alignment = 1 # 1: Town, 2: Werewolf, 3: Neutral + + # 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 unique = False # Only one of this role per game game_start_message = ( @@ -23,9 +29,9 @@ class Villager(Role): async def see_alignment(self, source=None): """ Interaction for investigative roles attempting - to see team (Village, Werewolf Other) + to see team (Village, Werewolf, Other) """ - return "Village" + return ALIGNMENT_TOWN async def get_role(self, source=None): """ diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 742a890..d26dc38 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -33,7 +33,7 @@ class Werewolf(Cog): default_guild = { "role_id": None, "category_id": None, - "channel_id": None, + "channel_name": None, "log_channel_id": None, } From 029b6a51b141c4c21c425bf8d3f8b771042dd5ee Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 25 Sep 2020 15:59:29 -0400 Subject: [PATCH 26/43] black --- werewolf/player.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/werewolf/player.py b/werewolf/player.py index 201b781..7f10758 100644 --- a/werewolf/player.py +++ b/werewolf/player.py @@ -34,4 +34,7 @@ class Player: try: await self.member.send(message) # Lets do embeds later except discord.Forbidden: - await self.role.game.village_channel.send(f"Couldn't DM {self.mention}, uh oh", allowed_mentions=discord.AllowedMentions(users=[self.member])) + await self.role.game.village_channel.send( + f"Couldn't DM {self.mention}, uh oh", + allowed_mentions=discord.AllowedMentions(users=[self.member]), + ) From 03f0ef17be2d8b3ea937cfcc14eb22aa14ddf90c Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 25 Sep 2020 22:04:35 -0400 Subject: [PATCH 27/43] Fix aggressive refactor --- werewolf/werewolf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index d26dc38..742a890 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -33,7 +33,7 @@ class Werewolf(Cog): default_guild = { "role_id": None, "category_id": None, - "channel_name": None, + "channel_id": None, "log_channel_id": None, } From af4cd92488ac3aae415aff445a3a2673ae5d67e4 Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 25 Sep 2020 22:05:22 -0400 Subject: [PATCH 28/43] Correctly reset channels --- werewolf/game.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index 13db415..df345af 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -8,6 +8,7 @@ from typing import List, Any, Dict, Set, Union import discord from redbot.core import commands from redbot.core.bot import Red +from redbot.core.utils import AsyncIter from werewolf.builder import parse_code from werewolf.player import Player @@ -913,10 +914,13 @@ class Game: try: await self.village_channel.edit(reason=reason, name="Werewolf") - for target, overwrites in self.save_perms[self.village_channel]: - await self.village_channel.set_permissions( - target, overwrite=overwrites, reason=reason - ) + async for channel, overwrites in AsyncIter(self.save_perms.items()): + async for target, overwrite in AsyncIter(overwrites.items()): + await channel.set_permissions(target, overwrite=overwrite, reason=reason) + # for target, overwrites in self.save_perms[self.village_channel]: + # await self.village_channel.set_permissions( + # target, overwrite=overwrites, reason=reason + # ) await self.village_channel.set_permissions( self.game_role, overwrite=None, reason=reason ) From 8a42b87bd63e0c1660aadef4cb5e76b4ab12159d Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 28 Sep 2020 10:54:47 -0400 Subject: [PATCH 29/43] black --- werewolf/builder.py | 2 +- werewolf/game.py | 5 ++--- werewolf/roles/seer.py | 10 +++++++--- werewolf/votegroups/wolfvote.py | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/werewolf/builder.py b/werewolf/builder.py index 4c803cc..f57a669 100644 --- a/werewolf/builder.py +++ b/werewolf/builder.py @@ -27,7 +27,7 @@ log = logging.getLogger("red.fox_v3.werewolf.builder") ROLE_DICT = {name: cls for name, cls in roles.__dict__.items() if isinstance(cls, type)} ROLE_LIST = sorted( [cls for cls in ROLE_DICT.values()], - key=attrgetter('alignment'), + key=attrgetter("alignment"), ) log.debug(f"{ROLE_DICT=}") diff --git a/werewolf/game.py b/werewolf/game.py index df345af..79d8455 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -1,9 +1,8 @@ import asyncio -import inspect import logging import random from collections import deque -from typing import List, Any, Dict, Set, Union +from typing import Dict, List, Union import discord from redbot.core import commands @@ -338,7 +337,7 @@ class Game: if check(): return await self.village_channel.send( - embed=discord.Embed(title=f"**{HALF_DAY_LENGTH/60} minutes of daylight remain...**") + embed=discord.Embed(title=f"**{HALF_DAY_LENGTH / 60} minutes of daylight remain...**") ) await asyncio.sleep(HALF_DAY_LENGTH) # 4 minute days FixMe to 120 later diff --git a/werewolf/roles/seer.py b/werewolf/roles/seer.py index 32ace18..f6bd857 100644 --- a/werewolf/roles/seer.py +++ b/werewolf/roles/seer.py @@ -1,7 +1,11 @@ import logging -from werewolf.constants import ALIGNMENT_TOWN, ALIGNMENT_WEREWOLF, CATEGORY_TOWN_INVESTIGATIVE, \ - CATEGORY_TOWN_RANDOM +from werewolf.constants import ( + ALIGNMENT_TOWN, + ALIGNMENT_WEREWOLF, + CATEGORY_TOWN_INVESTIGATIVE, + CATEGORY_TOWN_RANDOM, +) from werewolf.listener import wolflistener from werewolf.night_powers import pick_target from werewolf.role import Role @@ -91,7 +95,7 @@ class Seer(Role): if alignment == ALIGNMENT_WEREWOLF: out = "Your insight reveals this player to be a **Werewolf!**" - else: # Don't reveal neutrals + else: # Don't reveal neutrals out = "You fail to find anything suspicious about this player..." await self.player.send_dm(out) diff --git a/werewolf/votegroups/wolfvote.py b/werewolf/votegroups/wolfvote.py index 7f6bbde..75fb01c 100644 --- a/werewolf/votegroups/wolfvote.py +++ b/werewolf/votegroups/wolfvote.py @@ -69,5 +69,5 @@ class WolfVote(VoteGroup): await self.channel.send( "{} has voted to kill {}".format(author.mention, target.member.display_name), - allowed_mentions=discord.AllowedMentions(everyone=False, users=[author]) + allowed_mentions=discord.AllowedMentions(everyone=False, users=[author]), ) From 3a6d3df374e49bd0005e2cec2c7f456f3900f9ad Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 28 Sep 2020 17:24:35 -0400 Subject: [PATCH 30/43] More WIP progress --- werewolf/game.py | 84 ++++++++++++++++++++++--------- werewolf/player.py | 3 ++ werewolf/role.py | 4 +- werewolf/roles/seer.py | 1 + werewolf/roles/vanillawerewolf.py | 15 ------ werewolf/roles/villager.py | 6 --- werewolf/votegroup.py | 1 - werewolf/werewolf.py | 22 +++++++- 8 files changed, 88 insertions(+), 48 deletions(-) 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: From 2ab87866ddf2d4056c6d5266a4db733befe98d1c Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 12:41:15 -0400 Subject: [PATCH 31/43] Adjust to italics, fix generate targets to be more obvious and readable, fix `night_messages` confusion with `night_results` --- werewolf/game.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index ee84b09..c5aaad6 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -42,12 +42,12 @@ class Game: "votegroup": None, # uninitialized VoteGroup } - morning_messages = [ - "**The sun rises on day {} in the village..**", - "**Morning has arrived on day {}..**", + day_start_messages = [ + "*The sun rises on day {} in the village..*", + "*Morning has arrived on day {}..*", ] - night_messages = ["**Dawn falls on day {}..****"] + day_end_messages = ["*Dawn falls..*", "*The sun sets on the village*"] day_vote_count = 3 @@ -305,9 +305,9 @@ class Game: self.action_queue.append(self._at_day_start()) while self.action_queue and not self.game_over: - current_action = asyncio.create_task(self.action_queue.popleft()) + self.current_action = asyncio.create_task(self.action_queue.popleft()) try: - await current_action + await self.current_action except asyncio.CancelledError: log.debug("Cancelled task") # @@ -337,7 +337,7 @@ class Game: self.day_count += 1 # Print the results of who died during the night - embed = discord.Embed(title=random.choice(self.morning_messages).format(self.day_count)) + embed = discord.Embed(title=random.choice(self.day_start_messages).format(self.day_count)) for result in self.night_results: embed.add_field(name=result, value="________", inline=False) @@ -362,7 +362,7 @@ class Game: if check(): return await self.village_channel.send( - embed=discord.Embed(title=f"**{HALF_DAY_LENGTH / 60} minutes of daylight remain...**") + embed=discord.Embed(title=f"*{HALF_DAY_LENGTH / 60} minutes of daylight remain...*") ) await asyncio.sleep(HALF_DAY_LENGTH) # 4 minute days FixMe to 120 later @@ -386,7 +386,7 @@ class Game: await self.speech_perms(self.village_channel, target.member) # Only target can talk await self.village_channel.send( - f"**{target.mention} will be put to trial and has 30 seconds to defend themselves**", + f"*{target.mention} will be put to trial and has 30 seconds to defend themselves**", allowed_mentions=discord.AllowedMentions(everyone=False, users=[target]), ) @@ -480,7 +480,7 @@ class Game: await self.night_perms(self.village_channel) await self.village_channel.send( - embed=discord.Embed(title="**The sun sets on the village...**") + embed=discord.Embed(title=random.choice(self.day_end_messages)) ) await self._notify("at_day_end") @@ -546,7 +546,7 @@ class Game: # ###########END Notify structure############ async def generate_targets(self, channel, with_roles=False): - embed = discord.Embed(title="Remaining Players") + embed = discord.Embed(title="Remaining Players", description="[ID] - [Name]") for i, player in enumerate(self.players): if player.alive: status = "" @@ -554,15 +554,14 @@ class Game: status = "*[Dead]*-" if with_roles or not player.alive: embed.add_field( - name=f"ID# **{i}**", - value=f"{status}{player.member.display_name}-{player.role}", - inline=True, + name=f"{i} - {status}{player.member.display_name}", + value=f"{player.role}", + inline=False, ) else: embed.add_field( - name=f"ID# **{i}**", - value=f"{status}{player.member.display_name}", - inline=True, + name=f"{i} - {status}{player.member.display_name}", + inline=False, ) return await channel.send(embed=embed) From c7d320ccaa351c383968330faf21dda696ac4958 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 12:41:26 -0400 Subject: [PATCH 32/43] WIP Player converter --- werewolf/converters.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 werewolf/converters.py diff --git a/werewolf/converters.py b/werewolf/converters.py new file mode 100644 index 0000000..376749d --- /dev/null +++ b/werewolf/converters.py @@ -0,0 +1,28 @@ +from typing import TYPE_CHECKING, Union + +import discord +from discord.ext.commands import BadArgument, Converter +from redbot.core import commands + +from werewolf.player import Player + +if TYPE_CHECKING: + PlayerConverter = Union[int, discord.Member] + CronConverter = str +else: + + class PlayerConverter(Converter): + async def convert(self, ctx, argument) -> Player: + + try: + target = await commands.MemberConverter().convert(ctx, argument) + except BadArgument: + try: + target = int(argument) + assert target >= 0 + except (ValueError, AssertionError): + raise BadArgument + + # TODO: Get the game for context without making a new one + # TODO: Get player from game based on either ID or member object + return target \ No newline at end of file From 443c84ccabd38fed7c7a06a4939b24708d527c86 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 12:41:43 -0400 Subject: [PATCH 33/43] Fix `night_messages` to `night_results` --- werewolf/roles/blob.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/werewolf/roles/blob.py b/werewolf/roles/blob.py index bd7b598..af18983 100644 --- a/werewolf/roles/blob.py +++ b/werewolf/roles/blob.py @@ -98,4 +98,4 @@ class TheBlob(Role): if target is not None: target.role.properties["been_blobbed"] = True - self.game.night_messages.append("The Blob grows...") + self.game.night_results.append("The Blob grows...") From 211df56e1b6070196c226728b206b0f935ae529a Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 12:41:57 -0400 Subject: [PATCH 34/43] Add repr --- werewolf/votegroup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/werewolf/votegroup.py b/werewolf/votegroup.py index d8411fb..e651eda 100644 --- a/werewolf/votegroup.py +++ b/werewolf/votegroup.py @@ -22,6 +22,9 @@ class VoteGroup(WolfListener): self.vote_results = {} self.properties = {} # Extra data for other options + def __repr__(self): + return f"{self.__class__.__name__}({self.channel},{self.players})" + @wolflistener("at_game_start", priority=1) async def _at_game_start(self): await self.channel.send(" ".join(player.mention for player in self.players)) From e27cfba763b4d596fbed7568aeb64ed5c8389b2d Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 12:42:05 -0400 Subject: [PATCH 35/43] Move to italics --- werewolf/votegroups/wolfvote.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/werewolf/votegroups/wolfvote.py b/werewolf/votegroups/wolfvote.py index 75fb01c..d637c87 100644 --- a/werewolf/votegroups/wolfvote.py +++ b/werewolf/votegroups/wolfvote.py @@ -55,10 +55,10 @@ class WolfVote(VoteGroup): if target_id is not None and self.killer: await self.game.kill(target_id, self.killer, random.choice(self.kill_messages)) await self.channel.send( - "**{} has left to complete the kill...**".format(self.killer.member.display_name) + "*{} has left to complete the kill...*".format(self.killer.member.display_name) ) else: - await self.channel.send("**No kill will be attempted tonight...**") + await self.channel.send("*No kill will be attempted tonight...*") async def vote(self, target, author, target_id): """ From f3965b73d831bd86c36d38d270b5f38259057fac Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 14:25:19 -0400 Subject: [PATCH 36/43] Mostly messaging adjustments, fix for failing to talley votes --- werewolf/game.py | 57 ++++++++++++++++------------- werewolf/player.py | 6 +++- werewolf/werewolf.py | 85 ++++++++++++++++++++++++++++---------------- 3 files changed, 91 insertions(+), 57 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index c5aaad6..c0a9db4 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -10,13 +10,15 @@ from redbot.core.bot import Red from redbot.core.utils import AsyncIter from werewolf.builder import parse_code +from werewolf.constants import ALIGNMENT_NEUTRAL from werewolf.player import Player from werewolf.role import Role from werewolf.votegroup import VoteGroup log = logging.getLogger("red.fox_v3.werewolf.game") -HALF_DAY_LENGTH = 24 # FixMe: to 120 later for 4 minute days +HALF_DAY_LENGTH = 60 # FixMe: Make configurable +HALF_NIGHT_LENGTH = 60 async def anyone_has_role( @@ -167,7 +169,7 @@ class Game: 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( + await ctx.maybe_send_embed( f"Unable to add role **{self.game_role.name}**\n" f"Bot is missing `manage_roles` permissions" ) @@ -210,7 +212,7 @@ class Game: category=self.channel_category, ) except discord.Forbidden: - await ctx.send( + await ctx.maybe_send_embed( "Unable to create Game Channel and none was provided\n" "Grant Bot appropriate permissions or assign a game_channel" ) @@ -225,7 +227,7 @@ class Game: ) except discord.Forbidden as e: log.exception("Unable to rename Game Channel") - await ctx.send("Unable to rename Game Channel, ignoring") + await ctx.maybe_send_embed("Unable to rename Game Channel, ignoring") try: for target, ow in overwrite.items(): @@ -235,7 +237,7 @@ class Game: target=target, overwrite=curr, reason="(BOT) New game of werewolf" ) except discord.Forbidden: - await ctx.send( + await ctx.maybe_send_embed( "Unable to edit Game Channel permissions\n" "Grant Bot appropriate permissions to manage permissions" ) @@ -406,14 +408,17 @@ class Game: await vote_message.add_reaction("👎") await asyncio.sleep(15) - reaction_list = vote_message.reactions - if True: # TODO: Allow customizable vote history deletion. - await vote_message.delete() + # Refetch for reactions + vote_message = await self.village_channel.fetch_message(id=vote_message.id) + reaction_list = vote_message.reactions raw_up_votes = sum(p for p in reaction_list if p.emoji == "👍" and not p.me) raw_down_votes = sum(p for p in reaction_list if p.emoji == "👎" and not p.me) + if True: # TODO: Allow customizable vote history deletion. + await vote_message.delete() + # TODO: Support vote count modifying roles. (Need notify and count function) voted_to_lynch = raw_down_votes > raw_up_votes @@ -492,13 +497,13 @@ class Game: return await self._notify("at_night_start") - await asyncio.sleep(12) # 2 minutes FixMe to 120 later + await asyncio.sleep(HALF_NIGHT_LENGTH) # 2 minutes FixMe to 120 later await self.village_channel.send( - embed=discord.Embed(title="**Two minutes of night remain...**") + embed=discord.Embed(title=f"**{HALF_NIGHT_LENGTH / 60} minutes of night remain...**") ) - await asyncio.sleep(9) # 1.5 minutes FixMe to 90 later + await asyncio.sleep(HALF_NIGHT_LENGTH) # 1.5 minutes FixMe to 90 later await self.village_channel.send( - embed=discord.Embed(title="**Thirty seconds until sunrise...**") + embed=discord.Embed(title=f"**{HALF_NIGHT_LENGTH / 60} minutes until sunrise...**") ) await asyncio.sleep(3) # .5 minutes FixMe to 30 Later @@ -560,8 +565,7 @@ class Game: ) else: embed.add_field( - name=f"{i} - {status}{player.member.display_name}", - inline=False, + name=f"{i} - {status}{player.member.display_name}", inline=False, value="" ) return await channel.send(embed=embed) @@ -585,16 +589,16 @@ class Game: if votegroup is not None: self.p_channels[channel_id]["votegroup"] = votegroup - async def join(self, member: discord.Member, channel: discord.TextChannel): + async def join(self, ctx, member: discord.Member): """ Have a member join a game """ if self.started: - await channel.send("**Game has already started!**") + await ctx.maybe_send_embed("**Game has already started!**") return if await self.get_player_by_member(member) is not None: - await channel.send(f"{member.display_name} is already in the game!") + await ctx.maybe_send_embed(f"{member.display_name} is already in the game!") return self.players.append(Player(member)) @@ -609,7 +613,7 @@ class Game: # f"Bot is missing `manage_roles` permissions" # ) - await channel.send( + await ctx.maybe_send_embed( f"{member.display_name} has been added to the game, " f"total players is **{len(self.players)}**" ) @@ -645,15 +649,15 @@ class Game: player = await self.get_player_by_member(ctx.author) if player is None: - await ctx.send("You're not in this game!") + await ctx.maybe_send_embed("You're not in this game!") return if not player.alive: - await ctx.send("**Corpses** can't participate...") + await ctx.maybe_send_embed("**Corpses** can't participate...") return if player.role.blocked: - await ctx.send("Something is preventing you from doing this...") + await ctx.maybe_send_embed("Something is preventing you from doing this...") return # Let role do target validation, might be alternate targets @@ -821,7 +825,7 @@ class Game: async def set_code(self, ctx: commands.Context, game_code): if game_code is not None: self.game_code = game_code - await ctx.send("Code has been set") + await ctx.maybe_send_embed("Code has been set") async def get_roles(self, ctx, game_code=None): if game_code is not None: @@ -833,10 +837,12 @@ class Game: try: self.roles = await parse_code(self.game_code, self) except ValueError as e: - await ctx.send("Invalid Code: Code contains unknown character\n{}".format(e)) + await ctx.maybe_send_embed( + "Invalid Code: Code contains unknown character\n{}".format(e) + ) return False except IndexError as e: - await ctx.send("Invalid Code: Code references unknown role\n{}".format(e)) + await ctx.maybe_send_embed("Invalid Code: Code references unknown role\n{}".format(e)) if not self.roles: return False @@ -898,7 +904,8 @@ class Game: self.game_over = True alignment1 = alive_players[0].role.alignment alignment2 = alive_players[1].role.alignment - if alignment1 == alignment2: # Same team + # Same team and not neutral + if alignment1 == alignment2 and alignment1 != ALIGNMENT_NEUTRAL: winners = alive_players else: winners = [max(alive_players, key=lambda p: p.role.alignment)] diff --git a/werewolf/player.py b/werewolf/player.py index 48885a8..7aec179 100644 --- a/werewolf/player.py +++ b/werewolf/player.py @@ -35,9 +35,13 @@ class Player: async def send_dm(self, message): try: - await self.member.send(message) # Lets do embeds later + await self.member.send(message) # Lets ToDo embeds later except discord.Forbidden: + log.info(f"Unable to mention {self.member.__repr__()}") await self.role.game.village_channel.send( f"Couldn't DM {self.mention}, uh oh", allowed_mentions=discord.AllowedMentions(users=[self.member]), ) + except AttributeError: + log.exception("Someone messed up and added a bot to the game (I think)") + await self.role.game.village_channel.send("Someone messed up and added a bot to the game :eyes:") diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 599796c..dc27338 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -72,9 +72,9 @@ class Werewolf(Cog): code = await gb.build_game(ctx) if code != "": - await ctx.send(f"Your game code is **{code}**") + await ctx.maybe_send_embed(f"Your game code is **{code}**") else: - await ctx.send("No code generated") + await ctx.maybe_send_embed("No code generated") @checks.guildowner() @commands.group() @@ -117,10 +117,10 @@ class Werewolf(Cog): """ if role is None: await self.config.guild(ctx.guild).role_id.set(None) - await ctx.send("Cleared Game Role") + await ctx.maybe_send_embed("Cleared Game Role") else: await self.config.guild(ctx.guild).role_id.set(role.id) - await ctx.send("Game Role has been set to **{}**".format(role.name)) + await ctx.maybe_send_embed("Game Role has been set to **{}**".format(role.name)) @commands.guild_only() @wwset.command(name="category") @@ -130,14 +130,14 @@ class Werewolf(Cog): """ if category_id is None: await self.config.guild(ctx.guild).category_id.set(None) - await ctx.send("Cleared Game Channel Category") + await ctx.maybe_send_embed("Cleared Game Channel Category") else: category = discord.utils.get(ctx.guild.categories, id=int(category_id)) if category is None: - await ctx.send("Category not found") + await ctx.maybe_send_embed("Category not found") return await self.config.guild(ctx.guild).category_id.set(category.id) - await ctx.send("Game Channel Category has been set to **{}**".format(category.name)) + await ctx.maybe_send_embed("Game Channel Category has been set to **{}**".format(category.name)) @commands.guild_only() @wwset.command(name="channel") @@ -147,10 +147,10 @@ class Werewolf(Cog): """ if channel is None: await self.config.guild(ctx.guild).channel_id.set(None) - await ctx.send("Cleared Game Channel") + await ctx.maybe_send_embed("Cleared Game Channel") else: await self.config.guild(ctx.guild).channel_id.set(channel.id) - await ctx.send("Game Channel has been set to **{}**".format(channel.mention)) + await ctx.maybe_send_embed("Game Channel has been set to **{}**".format(channel.mention)) @commands.guild_only() @wwset.command(name="logchannel") @@ -160,10 +160,10 @@ class Werewolf(Cog): """ if channel is None: await self.config.guild(ctx.guild).log_channel_id.set(None) - await ctx.send("Cleared Game Log Channel") + await ctx.maybe_send_embed("Cleared Game Log Channel") else: await self.config.guild(ctx.guild).log_channel_id.set(channel.id) - await ctx.send("Game Log Channel has been set to **{}**".format(channel.mention)) + await ctx.maybe_send_embed("Game Log Channel has been set to **{}**".format(channel.mention)) @commands.group() async def ww(self, ctx: commands.Context): @@ -181,9 +181,9 @@ class Werewolf(Cog): """ game = await self._get_game(ctx, game_code) if not game: - await ctx.send("Failed to start a new game") + await ctx.maybe_send_embed("Failed to start a new game") else: - await ctx.send("Game is ready to join! Use `[p]ww join`") + await ctx.maybe_send_embed("Game is ready to join! Use `[p]ww join`") @commands.guild_only() @ww.command(name="join") @@ -195,10 +195,27 @@ class Werewolf(Cog): game: Game = await self._get_game(ctx) if not game: - await ctx.send("No game to join!\nCreate a new one with `[p]ww new`") + await ctx.maybe_send_embed("Failed to join a game!") return - await game.join(ctx.author, ctx.channel) + await game.join(ctx, ctx.author) + await ctx.tick() + + @commands.guild_only() + @commands.admin() + @ww.command(name="forcejoin") + async def ww_forcejoin(self, ctx: commands.Context, target: discord.Member): + """ + Force someone to join a game of Werewolf + """ + + game: Game = await self._get_game(ctx) + + if not game: + await ctx.maybe_send_embed("Failed to join a game!") + return + + await game.join(ctx, target) await ctx.tick() @commands.guild_only() @@ -213,7 +230,7 @@ class Werewolf(Cog): game = await self._get_game(ctx) if not game: - await ctx.send("No game to join!\nCreate a new one with `[p]ww new`") + await ctx.maybe_send_embed("No game to join!\nCreate a new one with `[p]ww new`") return await game.set_code(ctx, code) @@ -239,7 +256,7 @@ class Werewolf(Cog): """ game = await self._get_game(ctx) if not game: - await ctx.send("No game running, cannot start") + await ctx.maybe_send_embed("No game running, cannot start") if not await game.setup(ctx): pass # ToDo something? @@ -257,13 +274,13 @@ class Werewolf(Cog): # await ctx.send("Cannot stop game from PM!") # return if ctx.guild.id not in self.games or self.games[ctx.guild.id].game_over: - await ctx.send("No game to stop") + await ctx.maybe_send_embed("No game to stop") return game = await self._get_game(ctx) game.game_over = True await game.current_action.cancel() - await ctx.send("Game has been stopped") + await ctx.maybe_send_embed("Game has been stopped") @commands.guild_only() @ww.command(name="vote") @@ -277,7 +294,7 @@ class Werewolf(Cog): target_id = None if target_id is None: - await ctx.send("`id` must be an integer") + await ctx.maybe_send_embed("`id` must be an integer") return # if ctx.guild is None: @@ -294,7 +311,7 @@ class Werewolf(Cog): game = await self._get_game(ctx) if game is None: - await ctx.send("No game running, cannot vote") + await ctx.maybe_send_embed("No game running, cannot vote") return # Game handles response now @@ -304,7 +321,7 @@ class Werewolf(Cog): elif channel in (c["channel"] for c in game.p_channels.values()): await game.vote(ctx.author, target_id, channel) else: - await ctx.send("Nothing to vote for in this channel") + await ctx.maybe_send_embed("Nothing to vote for in this channel") @ww.command(name="choose") async def ww_choose(self, ctx: commands.Context, data): @@ -315,7 +332,7 @@ class Werewolf(Cog): """ if ctx.guild is not None: - await ctx.send("This action is only available in DM's") + await ctx.maybe_send_embed("This action is only available in DM's") return # DM nonsense, find their game # If multiple games, panic @@ -323,7 +340,7 @@ class Werewolf(Cog): if await game.get_player_by_member(ctx.author): break # game = game else: - await ctx.send("You're not part of any werewolf game") + await ctx.maybe_send_embed("You're not part of any werewolf game") return await game.choose(ctx, data) @@ -344,7 +361,7 @@ class Werewolf(Cog): if from_name: await menu(ctx, from_name, DEFAULT_CONTROLS) else: - await ctx.send("No roles containing that name were found") + await ctx.maybe_send_embed("No roles containing that name were found") @ww_search.command(name="alignment") async def ww_search_alignment(self, ctx: commands.Context, alignment: int): @@ -354,7 +371,7 @@ class Werewolf(Cog): if from_alignment: await menu(ctx, from_alignment, DEFAULT_CONTROLS) else: - await ctx.send("No roles with that alignment were found") + await ctx.maybe_send_embed("No roles with that alignment were found") @ww_search.command(name="category") async def ww_search_category(self, ctx: commands.Context, category: int): @@ -364,7 +381,7 @@ class Werewolf(Cog): if pages: await menu(ctx, pages, DEFAULT_CONTROLS) else: - await ctx.send("No roles in that category were found") + await ctx.maybe_send_embed("No roles in that category were found") @ww_search.command(name="index") async def ww_search_index(self, ctx: commands.Context, idx: int): @@ -374,23 +391,29 @@ class Werewolf(Cog): if idx_embed is not None: await ctx.send(embed=idx_embed) else: - await ctx.send("Role ID not found") + await ctx.maybe_send_embed("Role ID not found") 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: # Private message, can't get guild - await ctx.send("Cannot start game from DM!") + await ctx.maybe_send_embed("Cannot start game from DM!") return None if guild.id not in self.games or self.games[guild.id].game_over: - await ctx.send("Starting a new game...") + await ctx.maybe_send_embed("Starting a new game...") valid, role, category, channel, log_channel = await self._get_settings(ctx) if not valid: - await ctx.send("Cannot start a new game") + await ctx.maybe_send_embed("Cannot start a new game") return None + who_has_the_role = await anyone_has_role(guild.members, role) + if who_has_the_role: + await ctx.maybe_send_embed( + f"Cannot continue, {who_has_the_role.display_name} already has the game role." + ) + return None self.games[guild.id] = Game( self.bot, guild, role, category, channel, log_channel, game_code ) From 9ca5d37f7e9bf318c33415e53476f89e99e1950c Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 17:17:11 -0400 Subject: [PATCH 37/43] Fixed a variable reuse, channel naming, bot's can't play, less bold, object deletion error catching --- werewolf/game.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index c0a9db4..bb77f02 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -221,8 +221,7 @@ class Game: self.save_perms[self.village_channel] = self.village_channel.overwrites try: await self.village_channel.edit( - name="🔵Werewolf", - category=self.channel_category, + name="🔵werewolf", reason="(BOT) New game of werewolf", ) except discord.Forbidden as e: @@ -298,7 +297,7 @@ class Game: _at_voted() _at_kill() _at_day_end() - _at_night_begin() + _at_night_start() _at_night_end() and repeat with _at_day_start() again @@ -331,6 +330,7 @@ class Game: if self.game_over: return + # await self.village_channel.edit(reason="WW Night Start", name="werewolf-🌞") self.action_queue.append(self._at_day_end()) # Get this ready in case day is cancelled def check(): @@ -413,6 +413,7 @@ class Game: vote_message = await self.village_channel.fetch_message(id=vote_message.id) reaction_list = vote_message.reactions + log.debug(f"Vote results: {[p.emoji.__repr__() for p in reaction_list]}") raw_up_votes = sum(p for p in reaction_list if p.emoji == "👍" and not p.me) raw_down_votes = sum(p for p in reaction_list if p.emoji == "👎" and not p.me) @@ -495,6 +496,9 @@ class Game: async def _at_night_start(self): # ID 6 if self.game_over: return + + # await self.village_channel.edit(reason="WW Night Start", name="werewolf-🌑") + await self._notify("at_night_start") await asyncio.sleep(HALF_NIGHT_LENGTH) # 2 minutes FixMe to 120 later @@ -502,9 +506,7 @@ class Game: embed=discord.Embed(title=f"**{HALF_NIGHT_LENGTH / 60} minutes of night remain...**") ) await asyncio.sleep(HALF_NIGHT_LENGTH) # 1.5 minutes FixMe to 90 later - await self.village_channel.send( - embed=discord.Embed(title=f"**{HALF_NIGHT_LENGTH / 60} minutes until sunrise...**") - ) + await asyncio.sleep(3) # .5 minutes FixMe to 30 Later self.action_queue.append(self._at_night_end()) @@ -522,11 +524,11 @@ class Game: return await self._notify("at_visit", target=target, source=source) - async def _notify(self, event, **kwargs): + async def _notify(self, event_name, **kwargs): for i in range(1, 7): # action guide 1-6 (0 is no action) tasks = [] - for event in self.listeners.get(event, {}).get(i, []): - tasks.append(asyncio.ensure_future(event(**kwargs), loop=self.loop)) + for event in self.listeners.get(event_name, {}).get(i, []): + tasks.append(asyncio.create_task(event(**kwargs))) # Run same-priority task simultaneously await asyncio.gather(*tasks) @@ -565,7 +567,7 @@ class Game: ) else: embed.add_field( - name=f"{i} - {status}{player.member.display_name}", inline=False, value="" + name=f"{i} - {status}{player.member.display_name}", inline=False, value="____" ) return await channel.send(embed=embed) @@ -594,7 +596,11 @@ class Game: Have a member join a game """ if self.started: - await ctx.maybe_send_embed("**Game has already started!**") + await ctx.maybe_send_embed("Game has already started!") + return + + if member.bot: + await ctx.maybe_send_embed("Bots can't play games") return if await self.get_player_by_member(member) is not None: @@ -942,10 +948,14 @@ class Game: reason = "(BOT) End of WW game" for obj in self.to_delete: log.debug(f"End_game: Deleting object {obj.__repr__()}") - await obj.delete(reason=reason) + try: + await obj.delete(reason=reason) + except discord.NotFound: + # Already deleted + pass try: - await self.village_channel.edit(reason=reason, name="Werewolf") + asyncio.create_task(self.village_channel.edit(reason=reason, name="werewolf")) async for channel, overwrites in AsyncIter(self.save_perms.items()): async for target, overwrite in AsyncIter(overwrites.items()): await channel.set_permissions(target, overwrite=overwrite, reason=reason) From 8a4893c5f54996c29c7f1a534675b5c8ffa94f21 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 17:17:20 -0400 Subject: [PATCH 38/43] Forgot the f in fstring --- werewolf/votegroups/wolfvote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/werewolf/votegroups/wolfvote.py b/werewolf/votegroups/wolfvote.py index d637c87..dfb4f32 100644 --- a/werewolf/votegroups/wolfvote.py +++ b/werewolf/votegroups/wolfvote.py @@ -51,7 +51,7 @@ class WolfVote(VoteGroup): if vote_list: target_id = max(set(vote_list), key=vote_list.count) - log.debug("Target id: {target_id}\nKiller: {self.killer.member.display_name}") + log.debug(f"Target id: {target_id}\nKiller: {self.killer.member.display_name}") if target_id is not None and self.killer: await self.game.kill(target_id, self.killer, random.choice(self.kill_messages)) await self.channel.send( From 19104241d71391ba4eb103e90343d49c0bbddea8 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 17:17:29 -0400 Subject: [PATCH 39/43] Don't await task cancels --- werewolf/werewolf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index dc27338..a9870ab 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -279,7 +279,7 @@ class Werewolf(Cog): game = await self._get_game(ctx) game.game_over = True - await game.current_action.cancel() + game.current_action.cancel() await ctx.maybe_send_embed("Game has been stopped") @commands.guild_only() From 62a70c52c63055ae88cb76ca71f6810073bf19fd Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 17:17:49 -0400 Subject: [PATCH 40/43] Some weird error with dm-ing keeps happening, add better log to catch it --- werewolf/role.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/werewolf/role.py b/werewolf/role.py index 0997b56..e267283 100644 --- a/werewolf/role.py +++ b/werewolf/role.py @@ -120,7 +120,11 @@ class Role(WolfListener): if self.channel_name: await self.game.register_channel(self.channel_name, self) - await self.player.send_dm(self.game_start_message) # Maybe embeds eventually + try: + await self.player.send_dm(self.game_start_message) # Maybe embeds eventually + except AttributeError as e: + log.exception(self.__repr__()) + raise e async def kill(self, source): """ From c529d792e6d9416ff8f093151755e0947099e916 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 17:31:55 -0400 Subject: [PATCH 41/43] Fix double game_end --- werewolf/game.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/werewolf/game.py b/werewolf/game.py index bb77f02..63b922a 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -929,7 +929,7 @@ class Game: await self._announce_winners(alive_players) # If no return, cleanup and end game - await self._end_game() + # await self._end_game() async def _announce_winners(self, winnerlist): await self.village_channel.send(self.game_role.mention) From 7f8d0f13f7b0ca9866a482fd40a893d72cc33a37 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 17:32:35 -0400 Subject: [PATCH 42/43] Black format --- werewolf/converters.py | 2 +- werewolf/player.py | 4 +++- werewolf/werewolf.py | 12 +++++++++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/werewolf/converters.py b/werewolf/converters.py index 376749d..f108666 100644 --- a/werewolf/converters.py +++ b/werewolf/converters.py @@ -25,4 +25,4 @@ else: # TODO: Get the game for context without making a new one # TODO: Get player from game based on either ID or member object - return target \ No newline at end of file + return target diff --git a/werewolf/player.py b/werewolf/player.py index 7aec179..c574109 100644 --- a/werewolf/player.py +++ b/werewolf/player.py @@ -44,4 +44,6 @@ class Player: ) except AttributeError: log.exception("Someone messed up and added a bot to the game (I think)") - await self.role.game.village_channel.send("Someone messed up and added a bot to the game :eyes:") + await self.role.game.village_channel.send( + "Someone messed up and added a bot to the game :eyes:" + ) diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index a9870ab..bd68a6f 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -137,7 +137,9 @@ class Werewolf(Cog): await ctx.maybe_send_embed("Category not found") return await self.config.guild(ctx.guild).category_id.set(category.id) - await ctx.maybe_send_embed("Game Channel Category has been set to **{}**".format(category.name)) + await ctx.maybe_send_embed( + "Game Channel Category has been set to **{}**".format(category.name) + ) @commands.guild_only() @wwset.command(name="channel") @@ -150,7 +152,9 @@ class Werewolf(Cog): await ctx.maybe_send_embed("Cleared Game Channel") else: await self.config.guild(ctx.guild).channel_id.set(channel.id) - await ctx.maybe_send_embed("Game Channel has been set to **{}**".format(channel.mention)) + await ctx.maybe_send_embed( + "Game Channel has been set to **{}**".format(channel.mention) + ) @commands.guild_only() @wwset.command(name="logchannel") @@ -163,7 +167,9 @@ class Werewolf(Cog): await ctx.maybe_send_embed("Cleared Game Log Channel") else: await self.config.guild(ctx.guild).log_channel_id.set(channel.id) - await ctx.maybe_send_embed("Game Log Channel has been set to **{}**".format(channel.mention)) + await ctx.maybe_send_embed( + "Game Log Channel has been set to **{}**".format(channel.mention) + ) @commands.group() async def ww(self, ctx: commands.Context): From 266b0a485d03d02f40640871d86255fa522e0760 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 29 Sep 2020 17:39:07 -0400 Subject: [PATCH 43/43] Alpha ready changes --- werewolf/game.py | 2 +- werewolf/info.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index 63b922a..668bf16 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -17,7 +17,7 @@ from werewolf.votegroup import VoteGroup log = logging.getLogger("red.fox_v3.werewolf.game") -HALF_DAY_LENGTH = 60 # FixMe: Make configurable +HALF_DAY_LENGTH = 90 # FixMe: Make configurable HALF_NIGHT_LENGTH = 60 diff --git a/werewolf/info.json b/werewolf/info.json index 99bc768..c8ef454 100644 --- a/werewolf/info.json +++ b/werewolf/info.json @@ -4,10 +4,10 @@ ], "min_bot_version": "3.3.0", "description": "Customizable Werewolf Game", - "hidden": true, + "hidden": false, "install_msg": "Thank you for installing Werewolf! Get started with `[p]load werewolf`\n Use `[p]wwset` to run inital setup", "requirements": [], - "short": "Werewolf Game", + "short": "[ALPHA] Play Werewolf (Mafia) Game in discord", "end_user_data_statement": "This stores user IDs in memory while they're actively using the cog, and stores no persistent End User Data.", "tags": [ "mafia",