Merge pull request #147 from bobloy/werewolf-develop
Werewolf Game ready for Alpha
This commit is contained in:
		
						commit
						20d8acc800
					
				| @ -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 | ||||
|  | ||||
| @ -1,5 +1,7 @@ | ||||
| import bisect | ||||
| import logging | ||||
| from collections import defaultdict | ||||
| from operator import attrgetter | ||||
| from random import choice | ||||
| 
 | ||||
| import discord | ||||
| @ -8,79 +10,57 @@ 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.constants import ROLE_CATEGORY_DESCRIPTIONS | ||||
| 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=attrgetter("alignment"), | ||||
| ) | ||||
| 
 | ||||
| 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]] | ||||
| log.debug(f"{ROLE_DICT=}") | ||||
| 
 | ||||
| # Town, Werewolf, Neutral | ||||
| ALIGNMENT_COLORS = [0x008000, 0xFF0000, 0xC0C0C0] | ||||
| 
 | ||||
| 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): | ||||
|     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) | ||||
| 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=False | ||||
|     ) | ||||
|     embed.add_field(name="Multiples Allowed", value=str(not role.unique), inline=False) | ||||
|     embed.add_field( | ||||
|         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=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="Town {}".format(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="Werewolf {}".format(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="RANDOM:Neutral Role", description="Neutral {}".format(v), color=0xc0c0c0)) | ||||
|             CATEGORY_COUNT.append(k) | ||||
| 
 | ||||
| 
 | ||||
| """ | ||||
| Example code: | ||||
| 0 = Villager | ||||
| @ -147,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: | ||||
| @ -187,49 +167,20 @@ 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.guild.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.guild.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]) | ||||
|             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 +193,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): | ||||
| @ -255,34 +209,87 @@ 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_CATEGORY_DESCRIPTIONS[role]}"] += 1 | ||||
|         if 10 < role <= 16: | ||||
|             role_dict["Werewolf {}".format(ROLE_CATEGORIES[role])] += 1 | ||||
|             role_dict[f"Werewolf {ROLE_CATEGORY_DESCRIPTIONS[role]}"] += 1 | ||||
|         if 20 < role <= 26: | ||||
|             role_dict["Neutral {}".format(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="Count: {}".format(v), inline=True) | ||||
|         embed.add_field(name=k, value=f"Count: {v}", inline=True) | ||||
| 
 | ||||
|     return embed | ||||
| 
 | ||||
| 
 | ||||
| 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, | ||||
|             "☑": self.select_page, | ||||
|             "➡": next_page, | ||||
|             '⏩': next_group, | ||||
|             '📇': self.list_roles, | ||||
|             "❌": close_menu | ||||
|             "⏩": self.next_group, | ||||
|             "📇": self.list_roles, | ||||
|             "❌": close_menu, | ||||
|         } | ||||
| 
 | ||||
|         await ctx.send("Browse through roles and add the ones you want using the check mark") | ||||
| @ -292,10 +299,17 @@ 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): | ||||
|         perms = message.channel.permissions_for(ctx.guild.me) | ||||
|     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: | ||||
|                 await message.remove_reaction(emoji, ctx.author) | ||||
| @ -304,13 +318,19 @@ 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) | ||||
|         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.guild.me) | ||||
|     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: | ||||
|                 await message.remove_reaction(emoji, ctx.author) | ||||
| @ -318,9 +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) | ||||
|         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) | ||||
|  | ||||
							
								
								
									
										91
									
								
								werewolf/constants.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								werewolf/constants.py
									
									
									
									
									
										Normal file
									
								
							| @ -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) | ||||
| """ | ||||
							
								
								
									
										28
									
								
								werewolf/converters.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								werewolf/converters.py
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
							
								
								
									
										647
									
								
								werewolf/game.py
									
									
									
									
									
								
							
							
						
						
									
										647
									
								
								werewolf/game.py
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -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", | ||||
|  | ||||
							
								
								
									
										106
									
								
								werewolf/listener.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								werewolf/listener.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,106 @@ | ||||
| import inspect | ||||
| 
 | ||||
| 
 | ||||
| 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`. | ||||
| 
 | ||||
|     Parameters | ||||
|     ------------ | ||||
|     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 | ||||
|     -------- | ||||
|     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__ = priority | ||||
|         to_assign = name or actual.__name__ | ||||
|         try: | ||||
|             actual.__wolf_listener_names__.append((priority, to_assign)) | ||||
|         except AttributeError: | ||||
|             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 | ||||
|         # thus the assignments need to be on the actual function | ||||
|         return func | ||||
| 
 | ||||
|     return decorator | ||||
| 
 | ||||
| 
 | ||||
| class WolfListenerMeta(type): | ||||
|     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__(mcs, name, bases, attrs, **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(base, elem)) | ||||
|                         listeners[elem] = value | ||||
| 
 | ||||
|         listeners_as_list = [] | ||||
|         for listener in listeners.values(): | ||||
|             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((priority, listener_name, listener.__name__)) | ||||
| 
 | ||||
|         new_cls.__wolf_listeners__ = listeners_as_list | ||||
|         return new_cls | ||||
| 
 | ||||
| 
 | ||||
| class WolfListener(metaclass=WolfListenerMeta): | ||||
|     def __init__(self, game): | ||||
|         for priority, name, method_name in self.__wolf_listeners__: | ||||
|             game.add_ww_listener(getattr(self, method_name), priority, name) | ||||
| @ -1,4 +1,8 @@ | ||||
| from .role import Role | ||||
| import logging | ||||
| 
 | ||||
| from werewolf.role import Role | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.werewolf.night_powers") | ||||
| 
 | ||||
| 
 | ||||
| def night_immune(role: Role): | ||||
|  | ||||
| @ -1,5 +1,9 @@ | ||||
| import logging | ||||
| 
 | ||||
| import discord | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.werewolf.player") | ||||
| 
 | ||||
| 
 | ||||
| class Player: | ||||
|     """ | ||||
| @ -16,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 | ||||
| @ -28,6 +35,15 @@ 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: | ||||
|             await self.role.game.village_channel.send("Couldn't DM {}, uh oh".format(self.mention)) | ||||
|             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:" | ||||
|             ) | ||||
|  | ||||
| @ -1,4 +1,12 @@ | ||||
| class Role: | ||||
| import inspect | ||||
| import logging | ||||
| 
 | ||||
| from werewolf.listener import WolfListener, wolflistener | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.werewolf.role") | ||||
| 
 | ||||
| 
 | ||||
| class Role(WolfListener): | ||||
|     """ | ||||
|     Base Role class for werewolf game | ||||
| 
 | ||||
| @ -18,9 +26,11 @@ class Role: | ||||
|         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 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) | ||||
| @ -33,13 +43,15 @@ 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) | ||||
|     # 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" | ||||
| @ -54,32 +66,14 @@ 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 | ||||
|         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) | ||||
|         return f"{self.__class__.__name__}({self.player.__repr__()})" | ||||
| 
 | ||||
|     async def assign_player(self, player): | ||||
|         """ | ||||
| @ -90,6 +84,8 @@ class Role: | ||||
|         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 | ||||
| @ -101,7 +97,7 @@ class 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" | ||||
| 
 | ||||
| @ -119,35 +115,16 @@ class Role: | ||||
|         """ | ||||
|         return "Default" | ||||
| 
 | ||||
|     async def _at_game_start(self, data=None): | ||||
|         if self.channel_id: | ||||
|             await self.game.register_channel(self.channel_id, self) | ||||
|     @wolflistener("at_game_start", priority=2) | ||||
|     async def _at_game_start(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 | ||||
|         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): | ||||
|         """ | ||||
|  | ||||
							
								
								
									
										11
									
								
								werewolf/roles/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								werewolf/roles/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| from .villager import Villager | ||||
| from .seer import Seer | ||||
| 
 | ||||
| from .vanillawerewolf import VanillaWerewolf | ||||
| 
 | ||||
| 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"] | ||||
							
								
								
									
										101
									
								
								werewolf/roles/blob.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								werewolf/roles/blob.py
									
									
									
									
									
										Normal file
									
								
							| @ -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 <ID>`\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_results.append("The Blob grows...") | ||||
| @ -1,11 +1,26 @@ | ||||
| from ..night_powers import pick_target | ||||
| from ..role import Role | ||||
| 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 | ||||
| 
 | ||||
| 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 | ||||
|     town_balance = 4 | ||||
|     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 = ( | ||||
| @ -14,8 +29,10 @@ class Seer(Role): | ||||
|         "Lynch players during the day with `[p]ww vote <ID>`\n" | ||||
|         "Check for werewolves at night with `[p]ww choose <ID>`" | ||||
|     ) | ||||
|     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) | ||||
| @ -24,47 +41,49 @@ 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): | ||||
|         """ | ||||
|         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" | ||||
| 
 | ||||
|     async def _at_night_start(self, data=None): | ||||
|     @wolflistener("at_night_start", priority=2) | ||||
|     async def _at_night_start(self): | ||||
|         if not self.player.alive: | ||||
|             return | ||||
|         self.see_target = None | ||||
|         await self.game.generate_targets(self.player.member) | ||||
|         await self.player.send_dm("**Pick a target to see tonight**") | ||||
| 
 | ||||
|     async def _at_night_end(self, data=None): | ||||
|     @wolflistener("at_night_end", priority=4) | ||||
|     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...") | ||||
| @ -75,9 +94,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) | ||||
| @ -87,4 +106,6 @@ 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( | ||||
|             f"**You will attempt to see the role of {target.member.display_name} tonight...**" | ||||
|         ) | ||||
|  | ||||
| @ -1,5 +1,11 @@ | ||||
| from ..night_powers import pick_target | ||||
| from ..role import Role | ||||
| 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 | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.werewolf.role.shifter") | ||||
| 
 | ||||
| 
 | ||||
| class Shifter(Role): | ||||
| @ -37,12 +43,13 @@ 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) | ||||
|     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 = ( | ||||
| @ -61,22 +68,22 @@ 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): | ||||
|         """ | ||||
|         Interaction for investigative roles attempting | ||||
|         to see alignment (Village, Werewolf, Other) | ||||
|         to see alignment (Village, Werewolf,, Other) | ||||
|         """ | ||||
|         return "Other" | ||||
| 
 | ||||
| @ -94,14 +101,14 @@ class Shifter(Role): | ||||
|         """ | ||||
|         return "Shifter" | ||||
| 
 | ||||
|     async def _at_night_start(self, data=None): | ||||
|         await super()._at_night_start(data) | ||||
|     @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**") | ||||
| 
 | ||||
|     async def _at_night_end(self, data=None): | ||||
|         await super()._at_night_end(data) | ||||
|     @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...") | ||||
| @ -114,16 +121,20 @@ 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( | ||||
|             f"**You will attempt to see the role of {target.member.display_name} tonight...**" | ||||
|         ) | ||||
|  | ||||
| @ -1,13 +1,19 @@ | ||||
| from ..role import Role | ||||
| import logging | ||||
| 
 | ||||
| from ..votegroups.wolfvote import WolfVote | ||||
| 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 | ||||
| 
 | ||||
| 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" | ||||
| @ -16,34 +22,19 @@ class VanillaWerewolf(Role): | ||||
|         "Vote to kill players at night with `[p]ww vote <ID>`" | ||||
|     ) | ||||
| 
 | ||||
|     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 | ||||
|         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): | ||||
|         """ | ||||
| @ -52,10 +43,13 @@ class VanillaWerewolf(Role): | ||||
|         """ | ||||
|         return "Werewolf" | ||||
| 
 | ||||
|     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 | ||||
|     @wolflistener("at_game_start", priority=2) | ||||
|     async def _at_game_start(self): | ||||
|         if self.channel_name: | ||||
|             log.debug("Wolf has channel_name: " + self.channel_name) | ||||
|             await self.game.register_channel( | ||||
|                 self.channel_name, self, WolfVote | ||||
|             )  # Add VoteGroup WolfVote | ||||
| 
 | ||||
|         await self.player.send_dm(self.game_start_message) | ||||
| 
 | ||||
|  | ||||
| @ -1,10 +1,17 @@ | ||||
| from ..role import Role | ||||
| 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 = ( | ||||
| @ -13,15 +20,12 @@ class Villager(Role): | ||||
|         "Lynch players during the day with `[p]ww vote <ID>`" | ||||
|     ) | ||||
| 
 | ||||
|     def __init__(self, game): | ||||
|         super().__init__(game) | ||||
| 
 | ||||
|     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): | ||||
|         """ | ||||
|  | ||||
| @ -1,4 +1,11 @@ | ||||
| class VoteGroup: | ||||
| import logging | ||||
| 
 | ||||
| from werewolf.listener import WolfListener, wolflistener | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.werewolf.votegroup") | ||||
| 
 | ||||
| 
 | ||||
| class VoteGroup(WolfListener): | ||||
|     """ | ||||
|     Base VoteGroup class for werewolf game | ||||
|     Handles secret channels and group decisions | ||||
| @ -8,58 +15,41 @@ 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) | ||||
|         ] | ||||
|     def __repr__(self): | ||||
|         return f"{self.__class__.__name__}({self.channel},{self.players})" | ||||
| 
 | ||||
|     async def on_event(self, event, data): | ||||
|         """ | ||||
|         See Game class for event guide | ||||
|         """ | ||||
| 
 | ||||
|         await self.action_list[event][0](data) | ||||
| 
 | ||||
|     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)) | ||||
| 
 | ||||
|     async def _at_day_start(self, data=None): | ||||
|         pass | ||||
|     @wolflistener("at_kill", priority=1) | ||||
|     async def _at_kill(self, player): | ||||
|         if player in self.players: | ||||
|             self.players.remove(player) | ||||
| 
 | ||||
|     async def _at_voted(self, data=None): | ||||
|         pass | ||||
|     @wolflistener("at_hang", priority=1) | ||||
|     async def _at_hang(self, player): | ||||
|         if player in self.players: | ||||
|             self.players.remove(player) | ||||
| 
 | ||||
|     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 | ||||
| 
 | ||||
|     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) | ||||
| 
 | ||||
|     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 | ||||
| 
 | ||||
| @ -70,11 +60,8 @@ class VoteGroup: | ||||
|             target = max(set(vote_list), key=vote_list.count) | ||||
| 
 | ||||
|         if target: | ||||
|             # Do what you voted on | ||||
|             pass | ||||
| 
 | ||||
|     async def _at_visit(self, data=None): | ||||
|         pass | ||||
|             # Do what the votegroup votes on | ||||
|             raise NotImplementedError | ||||
| 
 | ||||
|     async def register_players(self, *players): | ||||
|         """ | ||||
| @ -90,7 +77,7 @@ class VoteGroup: | ||||
|             self.players.remove(player) | ||||
| 
 | ||||
|         if not self.players: | ||||
|             # ToDo: Trigger deletion of votegroup | ||||
|             # TODO: Confirm deletion | ||||
|             pass | ||||
| 
 | ||||
|     async def vote(self, target, author, target_id): | ||||
|  | ||||
							
								
								
									
										1
									
								
								werewolf/votegroups/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								werewolf/votegroups/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| from .wolfvote import WolfVote | ||||
| @ -1,6 +1,12 @@ | ||||
| import logging | ||||
| import random | ||||
| 
 | ||||
| from ..votegroup import VoteGroup | ||||
| import discord | ||||
| 
 | ||||
| from werewolf.listener import wolflistener | ||||
| from werewolf.votegroup import VoteGroup | ||||
| 
 | ||||
| log = logging.getLogger("red.fox_v3.werewolf.votegroup.wolfvote") | ||||
| 
 | ||||
| 
 | ||||
| class WolfVote(VoteGroup): | ||||
| @ -13,71 +19,29 @@ 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) | ||||
|         # 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) | ||||
|         ] | ||||
|     @wolflistener("at_night_start", priority=2) | ||||
|     async def _at_night_start(self): | ||||
|         await super()._at_night_start() | ||||
| 
 | ||||
|         # 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 | ||||
| 
 | ||||
|         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) | ||||
|         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( | ||||
|             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 | ||||
| 
 | ||||
| @ -87,34 +51,23 @@ 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(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("**{} 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...**") | ||||
| 
 | ||||
|     # 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) | ||||
|             await self.channel.send("*No kill will be attempted tonight...*") | ||||
| 
 | ||||
|     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)) | ||||
|         await self.channel.send( | ||||
|             "{} has voted to kill {}".format(author.mention, target.member.display_name), | ||||
|             allowed_mentions=discord.AllowedMentions(everyone=False, users=[author]), | ||||
|         ) | ||||
|  | ||||
| @ -1,17 +1,31 @@ | ||||
| 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 .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") | ||||
| 
 | ||||
| 
 | ||||
| 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): | ||||
| @ -43,7 +57,7 @@ class Werewolf(Cog): | ||||
|         return | ||||
| 
 | ||||
|     def __unload(self): | ||||
|         print("Unload called") | ||||
|         log.debug("Unload called") | ||||
|         for game in self.games.values(): | ||||
|             del game | ||||
| 
 | ||||
| @ -58,9 +72,9 @@ class Werewolf(Cog): | ||||
|         code = await gb.build_game(ctx) | ||||
| 
 | ||||
|         if code != "": | ||||
|             await ctx.send("Your game code is **{}**".format(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() | ||||
| @ -77,31 +91,36 @@ class Werewolf(Cog): | ||||
|         """ | ||||
|         Lists current guild settings | ||||
|         """ | ||||
|         success, role, category, channel, log_channel = await self._get_settings(ctx) | ||||
|         if not success: | ||||
|             await ctx.send("Failed to get settings") | ||||
|             return None | ||||
|         valid, role, category, channel, log_channel = await self._get_settings(ctx) | ||||
|         # 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=0x008000 if valid else 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() | ||||
|     @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: | ||||
|             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") | ||||
| @ -111,14 +130,16 @@ 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") | ||||
| @ -128,10 +149,12 @@ 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") | ||||
| @ -141,10 +164,12 @@ 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): | ||||
| @ -162,9 +187,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") | ||||
| @ -173,28 +198,49 @@ 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`") | ||||
|             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() | ||||
|     @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) | ||||
| 
 | ||||
|         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) | ||||
|         await ctx.tick() | ||||
| 
 | ||||
|     @commands.guild_only() | ||||
|     @ww.command(name="quit") | ||||
| @ -206,6 +252,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") | ||||
| @ -215,10 +262,12 @@ 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  # Do something? | ||||
|             pass  # ToDo something? | ||||
| 
 | ||||
|         await ctx.tick() | ||||
| 
 | ||||
|     @commands.guild_only() | ||||
|     @ww.command(name="stop") | ||||
| @ -226,17 +275,18 @@ 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") | ||||
|             await ctx.maybe_send_embed("No game to stop") | ||||
|             return | ||||
| 
 | ||||
|         game = await self._get_game(ctx) | ||||
|         game.game_over = True | ||||
|         await ctx.send("Game has been stopped") | ||||
|         game.current_action.cancel() | ||||
|         await ctx.maybe_send_embed("Game has been stopped") | ||||
| 
 | ||||
|     @commands.guild_only() | ||||
|     @ww.command(name="vote") | ||||
| @ -250,7 +300,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: | ||||
| @ -267,7 +317,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 | ||||
| @ -277,7 +327,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): | ||||
| @ -288,7 +338,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 | ||||
| @ -296,7 +346,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) | ||||
| @ -317,7 +367,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): | ||||
| @ -327,7 +377,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): | ||||
| @ -337,7 +387,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): | ||||
| @ -347,24 +397,32 @@ 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): | ||||
|         guild: discord.Guild = ctx.guild | ||||
|     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 PM!") | ||||
|             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...") | ||||
|             success, role, category, channel, log_channel = await self._get_settings(ctx) | ||||
|             await ctx.maybe_send_embed("Starting a new game...") | ||||
|             valid, role, category, channel, log_channel = await self._get_settings(ctx) | ||||
| 
 | ||||
|             if not success: | ||||
|                 await ctx.send("Cannot start a new game") | ||||
|             if not valid: | ||||
|                 await ctx.maybe_send_embed("Cannot start a new game") | ||||
|                 return None | ||||
| 
 | ||||
|             self.games[guild.id] = Game(guild, role, category, channel, log_channel, game_code) | ||||
|             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 | ||||
|             ) | ||||
| 
 | ||||
|         return self.games[guild.id] | ||||
| 
 | ||||
| @ -385,23 +443,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 | ||||
|             # 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 | ||||
|         return ( | ||||
|             role is not None and category is not None and channel is not None, | ||||
|             role, | ||||
|             category, | ||||
|             channel, | ||||
|             log_channel, | ||||
|         ) | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 bobloy
						bobloy