Merge pull request #147 from bobloy/werewolf-develop

Werewolf Game ready for Alpha
pull/149/head
bobloy 4 years ago committed by GitHub
commit 20d8acc800
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -434,7 +434,7 @@ class Chatter(Cog):
else: else:
await ctx.maybe_send_embed("Error occurred :(") await ctx.maybe_send_embed("Error occurred :(")
@commands.Cog.listener() @Cog.listener()
async def on_message_without_command(self, message: discord.Message): async def on_message_without_command(self, message: discord.Message):
""" """
Credit to https://github.com/Twentysix26/26-Cogs/blob/master/cleverbot/cleverbot.py Credit to https://github.com/Twentysix26/26-Cogs/blob/master/cleverbot/cleverbot.py

@ -1,5 +1,7 @@
import bisect import bisect
import logging
from collections import defaultdict from collections import defaultdict
from operator import attrgetter
from random import choice from random import choice
import discord import discord
@ -8,77 +10,55 @@ import discord
# Import all roles here # Import all roles here
from redbot.core import commands from redbot.core import commands
from .roles.seer import Seer # from .roles.seer import Seer
from .roles.vanillawerewolf import VanillaWerewolf # from .roles.vanillawerewolf import VanillaWerewolf
from .roles.villager import Villager # from .roles.villager import Villager
from redbot.core.utils.menus import menu, prev_page, next_page, close_menu
# All roles in this list for iterating from werewolf import roles
from redbot.core.utils.menus import menu, prev_page, next_page, close_menu
ROLE_LIST = sorted([Villager, Seer, VanillaWerewolf], key=lambda x: x.alignment) from werewolf.constants import ROLE_CATEGORY_DESCRIPTIONS
from werewolf.role import Role
ALIGNMENT_COLORS = [0x008000, 0xff0000, 0xc0c0c0] log = logging.getLogger("red.fox_v3.werewolf.builder")
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]]
ROLE_PAGES = [] # All roles in this list for iterating
PAGE_GROUPS = [0]
ROLE_CATEGORIES = { ROLE_DICT = {name: cls for name, cls in roles.__dict__.items() if isinstance(cls, type)}
1: "Random", 2: "Investigative", 3: "Protective", 4: "Government", ROLE_LIST = sorted(
5: "Killing", 6: "Power (Special night action)", [cls for cls in ROLE_DICT.values()],
11: "Random", 12: "Deception", 15: "Killing", 16: "Support", key=attrgetter("alignment"),
21: "Benign", 22: "Evil", 23: "Killing"} )
CATEGORY_COUNT = [] log.debug(f"{ROLE_DICT=}")
# Town, Werewolf, Neutral
ALIGNMENT_COLORS = [0x008000, 0xFF0000, 0xC0C0C0]
def role_embed(idx, role, color): ROLE_PAGES = []
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
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)
def setup(): return embed
# 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)
""" """
@ -147,15 +127,15 @@ async def parse_code(code, game):
return decode return decode
async def encode(roles, rand_roles): async def encode(role_list, rand_roles):
"""Convert role list to code""" """Convert role list to code"""
out_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: for role in digit_sort:
out_code += str(role) 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: if digit_sort:
out_code += "-" out_code += "-"
for role in digit_sort: for role in digit_sort:
@ -187,49 +167,20 @@ async def encode(roles, rand_roles):
return out_code 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): def role_from_alignment(alignment):
return [role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]) return [
for idx, role in enumerate(ROLE_LIST) if alignment == role.alignment] 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): def role_from_category(category):
return [role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]) return [
for idx, role in enumerate(ROLE_LIST) if category in role.category] 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): def role_from_id(idx):
@ -242,8 +193,11 @@ def role_from_id(idx):
def role_from_name(name: str): def role_from_name(name: str):
return [role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1]) return [
for idx, role in enumerate(ROLE_LIST) if name in role.__name__] 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): 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: for role in rand_roles:
if 0 < role <= 6: 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: 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: 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(): 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 return embed
class GameBuilder: class GameBuilder:
def __init__(self): def __init__(self):
self.code = [] self.code = []
self.rand_roles = [] 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): async def build_game(self, ctx: commands.Context):
new_controls = { new_controls = {
'': prev_group, "": self.prev_group,
"": prev_page, "": prev_page,
'': self.select_page, "": self.select_page,
"": next_page, "": next_page,
'': next_group, "": self.next_group,
'📇': self.list_roles, "📇": self.list_roles,
"": close_menu "": close_menu,
} }
await ctx.send("Browse through roles and add the ones you want using the check mark") 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) out = await encode(self.code, self.rand_roles)
return out return out
async def list_roles(self, ctx: commands.Context, pages: list, async def list_roles(
controls: dict, message: discord.Message, page: int, self,
timeout: float, emoji: str): ctx: commands.Context,
perms = message.channel.permissions_for(ctx.guild.me) 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 if perms.manage_messages: # Can manage messages, so remove react
try: try:
await message.remove_reaction(emoji, ctx.author) 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)) await ctx.send(embed=say_role_list(self.code, self.rand_roles))
return await menu(ctx, pages, controls, message=message, return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout)
page=page, timeout=timeout)
async def select_page(
async def select_page(self, ctx: commands.Context, pages: list, self,
controls: dict, message: discord.Message, page: int, ctx: commands.Context,
timeout: float, emoji: str): pages: list,
perms = message.channel.permissions_for(ctx.guild.me) 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 if perms.manage_messages: # Can manage messages, so remove react
try: try:
await message.remove_reaction(emoji, ctx.author) await message.remove_reaction(emoji, ctx.author)
@ -318,9 +338,53 @@ class GameBuilder:
pass pass
if page >= len(ROLE_LIST): 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: else:
self.code.append(page) self.code.append(page)
return await menu(ctx, pages, controls, message=message, return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout)
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)

@ -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)
"""

@ -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

File diff suppressed because it is too large Load Diff

@ -4,10 +4,10 @@
], ],
"min_bot_version": "3.3.0", "min_bot_version": "3.3.0",
"description": "Customizable Werewolf Game", "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", "install_msg": "Thank you for installing Werewolf! Get started with `[p]load werewolf`\n Use `[p]wwset` to run inital setup",
"requirements": [], "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.", "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": [ "tags": [
"mafia", "mafia",

@ -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): def night_immune(role: Role):

@ -1,5 +1,9 @@
import logging
import discord import discord
log = logging.getLogger("red.fox_v3.werewolf.player")
class Player: class Player:
""" """
@ -16,6 +20,9 @@ class Player:
self.muted = False self.muted = False
self.protected = False self.protected = False
def __repr__(self):
return f"{self.__class__.__name__}({self.member})"
async def assign_role(self, role): async def assign_role(self, role):
""" """
Give this player a role Give this player a role
@ -28,6 +35,15 @@ class Player:
async def send_dm(self, message): async def send_dm(self, message):
try: try:
await self.member.send(message) # Lets do embeds later await self.member.send(message) # Lets ToDo embeds later
except discord.Forbidden: 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,31 +1,41 @@
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 Base Role class for werewolf game
Category enrollment guide as follows (category property): Category enrollment guide as follows (category property):
Town: Town:
1: Random, 2: Investigative, 3: Protective, 4: Government, 1: Random, 2: Investigative, 3: Protective, 4: Government,
5: Killing, 6: Power (Special night action) 5: Killing, 6: Power (Special night action)
Werewolf: Werewolf:
11: Random, 12: Deception, 15: Killing, 16: Support 11: Random, 12: Deception, 15: Killing, 16: Support
Neutral: Neutral:
21: Benign, 22: Evil, 23: Killing 21: Benign, 22: Evil, 23: Killing
Example category: Example category:
category = [1, 5, 6] Could be Veteran category = [1, 5, 6] Could be Veteran
category = [1, 5] Could be Bodyguard category = [1, 5] Could be Bodyguard
category = [11, 16] Could be Werewolf Silencer 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 _at_night_start
0. No Action 0. No Action
1. Detain actions (Jailer/Kidnapper) 1. Detain actions (Jailer/Kidnapper)
2. Group discussions and choose targets 2. Group discussions and choose targets
_at_night_end _at_night_end
0. No Action 0. No Action
1. Self actions (Veteran) 1. Self actions (Veteran)
@ -33,13 +43,15 @@ class Role:
3. Protection / Preempt actions (bodyguard/framer) 3. Protection / Preempt actions (bodyguard/framer)
4. Non-disruptive actions (seer/silencer) 4. Non-disruptive actions (seer/silencer)
5. Disruptive actions (Killing) 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) category = [0] # List of enrolled categories (listed above)
alignment = 0 # 1: Town, 2: Werewolf, 3: Neutral 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 unique = False # Only one of this role per game
game_start_message = ( game_start_message = (
"Your role is **Default**\n" "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 icon_url = None # Adding a URL here will enable a thumbnail of the role
def __init__(self, game): def __init__(self, game):
super().__init__(game)
self.game = game self.game = game
self.player = None self.player = None
self.blocked = False self.blocked = False
self.properties = {} # Extra data for other roles (i.e. arsonist) 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): def __repr__(self):
return self.__class__.__name__ return f"{self.__class__.__name__}({self.player.__repr__()})"
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): async def assign_player(self, player):
""" """
@ -90,6 +84,8 @@ class Role:
player.role = self player.role = self
self.player = player self.player = player
log.debug(f"Assigned {self} to {player}")
async def get_alignment(self, source=None): async def get_alignment(self, source=None):
""" """
Interaction for powerful access of alignment Interaction for powerful access of alignment
@ -101,7 +97,7 @@ class Role:
async def see_alignment(self, source=None): async def see_alignment(self, source=None):
""" """
Interaction for investigative roles attempting Interaction for investigative roles attempting
to see alignment (Village, Werewolf Other) to see alignment (Village, Werewolf, Other)
""" """
return "Other" return "Other"
@ -119,35 +115,16 @@ class Role:
""" """
return "Default" return "Default"
async def _at_game_start(self, data=None): @wolflistener("at_game_start", priority=2)
if self.channel_id: async def _at_game_start(self):
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): try:
pass await self.player.send_dm(self.game_start_message) # Maybe embeds eventually
except AttributeError as e:
async def _at_voted(self, data=None): log.exception(self.__repr__())
pass raise e
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): async def kill(self, source):
""" """

@ -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"]

@ -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 import logging
from ..role import Role
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): class Seer(Role):
rand_choice = True # Determines if it can be picked as a random role (False for unusually disruptive roles) rand_choice = True
category = [1, 2] # List of enrolled categories (listed above) town_balance = 4
alignment = 1 # 1: Town, 2: Werewolf, 3: Neutral 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 channel_id = "" # Empty for no private channel
unique = False # Only one of this role per game unique = False # Only one of this role per game
game_start_message = ( game_start_message = (
@ -14,8 +29,10 @@ class Seer(Role):
"Lynch players during the day with `[p]ww vote <ID>`\n" "Lynch players during the day with `[p]ww vote <ID>`\n"
"Check for werewolves at night with `[p]ww choose <ID>`" "Check for werewolves at night with `[p]ww choose <ID>`"
) )
description = "A mystic in search of answers in a chaotic town.\n" \ description = (
"Calls upon the cosmos to discern those of Lycan blood" "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): def __init__(self, game):
super().__init__(game) super().__init__(game)
@ -24,47 +41,49 @@ class Seer(Role):
# self.blocked = False # self.blocked = False
# self.properties = {} # Extra data for other roles (i.e. arsonist) # self.properties = {} # Extra data for other roles (i.e. arsonist)
self.see_target = None self.see_target = None
self.action_list = [ # self.action_list = [
(self._at_game_start, 1), # (Action, Priority) # (self._at_game_start, 1), # (Action, Priority)
(self._at_day_start, 0), # (self._at_day_start, 0),
(self._at_voted, 0), # (self._at_voted, 0),
(self._at_kill, 0), # (self._at_kill, 0),
(self._at_hang, 0), # (self._at_hang, 0),
(self._at_day_end, 0), # (self._at_day_end, 0),
(self._at_night_start, 2), # (self._at_night_start, 2),
(self._at_night_end, 4), # (self._at_night_end, 4),
(self._at_visit, 0) # (self._at_visit, 0),
] # ]
async def see_alignment(self, source=None): async def see_alignment(self, source=None):
""" """
Interaction for investigative roles attempting 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): async def get_role(self, source=None):
""" """
Interaction for powerful access of role Interaction for powerful access of role
Unlikely to be able to deceive this Unlikely to be able to deceive this
""" """
return "Villager" return "Seer"
async def see_role(self, source=None): async def see_role(self, source=None):
""" """
Interaction for investigative roles. Interaction for investigative roles.
More common to be able to deceive these 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: if not self.player.alive:
return return
self.see_target = None self.see_target = None
await self.game.generate_targets(self.player.member) await self.game.generate_targets(self.player.member)
await self.player.send_dm("**Pick a target to see tonight**") 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.see_target is None:
if self.player.alive: if self.player.alive:
await self.player.send_dm("You will not use your powers tonight...") await self.player.send_dm("You will not use your powers tonight...")
@ -75,9 +94,9 @@ class Seer(Role):
if target: if target:
alignment = await target.role.see_alignment(self.player) 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!**" 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..." out = "You fail to find anything suspicious about this player..."
await self.player.send_dm(out) await self.player.send_dm(out)
@ -87,4 +106,6 @@ class Seer(Role):
await super().choose(ctx, data) await super().choose(ctx, data)
self.see_target, target = await pick_target(self, 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,35 +1,41 @@
from ..night_powers import pick_target import logging
from ..role import Role
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): class Shifter(Role):
""" """
Base Role class for werewolf game Base Role class for werewolf game
Category enrollment guide as follows (category property): Category enrollment guide as follows (category property):
Town: Town:
1: Random, 2: Investigative, 3: Protective, 4: Government, 1: Random, 2: Investigative, 3: Protective, 4: Government,
5: Killing, 6: Power (Special night action) 5: Killing, 6: Power (Special night action)
Werewolf: Werewolf:
11: Random, 12: Deception, 15: Killing, 16: Support 11: Random, 12: Deception, 15: Killing, 16: Support
Neutral: Neutral:
21: Benign, 22: Evil, 23: Killing 21: Benign, 22: Evil, 23: Killing
Example category: Example category:
category = [1, 5, 6] Could be Veteran category = [1, 5, 6] Could be Veteran
category = [1, 5] Could be Bodyguard category = [1, 5] Could be Bodyguard
category = [11, 16] Could be Werewolf Silencer category = [11, 16] Could be Werewolf Silencer
Action guide as follows (on_event function): Action guide as follows (on_event function):
_at_night_start _at_night_start
0. No Action 0. No Action
1. Detain actions (Jailer/Kidnapper) 1. Detain actions (Jailer/Kidnapper)
2. Group discussions and choose targets 2. Group discussions and choose targets
_at_night_end _at_night_end
0. No Action 0. No Action
1. Self actions (Veteran) 1. Self actions (Veteran)
@ -37,12 +43,13 @@ class Shifter(Role):
3. Protection / Preempt actions (bodyguard/framer) 3. Protection / Preempt actions (bodyguard/framer)
4. Non-disruptive actions (seer/silencer) 4. Non-disruptive actions (seer/silencer)
5. Disruptive actions (Killing) 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) 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) town_balance = -3
alignment = 3 # 1: Town, 2: Werewolf, 3: Neutral 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 channel_id = "" # Empty for no private channel
unique = False # Only one of this role per game unique = False # Only one of this role per game
game_start_message = ( game_start_message = (
@ -61,22 +68,22 @@ class Shifter(Role):
super().__init__(game) super().__init__(game)
self.shift_target = None self.shift_target = None
self.action_list = [ # self.action_list = [
(self._at_game_start, 1), # (Action, Priority) # (self._at_game_start, 1), # (Action, Priority)
(self._at_day_start, 0), # (self._at_day_start, 0),
(self._at_voted, 0), # (self._at_voted, 0),
(self._at_kill, 0), # (self._at_kill, 0),
(self._at_hang, 0), # (self._at_hang, 0),
(self._at_day_end, 0), # (self._at_day_end, 0),
(self._at_night_start, 2), # Chooses targets # (self._at_night_start, 2), # Chooses targets
(self._at_night_end, 6), # Role Swap # (self._at_night_end, 6), # Role Swap
(self._at_visit, 0) # (self._at_visit, 0),
] # ]
async def see_alignment(self, source=None): async def see_alignment(self, source=None):
""" """
Interaction for investigative roles attempting Interaction for investigative roles attempting
to see alignment (Village, Werewolf, Other) to see alignment (Village, Werewolf,, Other)
""" """
return "Other" return "Other"
@ -94,14 +101,14 @@ class Shifter(Role):
""" """
return "Shifter" return "Shifter"
async def _at_night_start(self, data=None): @wolflistener("at_night_start", priority=2)
await super()._at_night_start(data) async def _at_night_start(self):
self.shift_target = None self.shift_target = None
await self.game.generate_targets(self.player.member) await self.game.generate_targets(self.player.member)
await self.player.send_dm("**Pick a target to shift into**") await self.player.send_dm("**Pick a target to shift into**")
async def _at_night_end(self, data=None): @wolflistener("at_night_end", priority=6)
await super()._at_night_end(data) async def _at_night_end(self):
if self.shift_target is None: if self.shift_target is None:
if self.player.alive: if self.player.alive:
await self.player.send_dm("You will not use your powers tonight...") await self.player.send_dm("You will not use your powers tonight...")
@ -114,16 +121,20 @@ class Shifter(Role):
# Roles have now been swapped # Roles have now been swapped
await self.player.send_dm("Your role has been stolen...\n" await self.player.send_dm(
"You are now a **Shifter**.") "Your role has been stolen...\n" "You are now a **Shifter**."
)
await self.player.send_dm(self.game_start_message) await self.player.send_dm(self.game_start_message)
await target.send_dm(target.role.game_start_message) await target.send_dm(target.role.game_start_message)
else: else:
await self.player.send_dm("**Your shift failed...**") await self.player.send_dm("**Your shift failed...**")
async def choose(self, ctx, data): async def choose(self, ctx, data):
"""Handle night actions""" """Handle night actions"""
await super().choose(ctx, data) await super().choose(ctx, data)
self.shift_target, target = await pick_target(self, 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): class VanillaWerewolf(Role):
rand_choice = True rand_choice = True
category = [11, 15] town_balance = -6
alignment = 2 # 1: Town, 2: Werewolf, 3: Neutral category = [CATEGORY_WW_RANDOM, CATEGORY_WW_KILLING]
channel_id = "werewolves" alignment = ALIGNMENT_WEREWOLF # 1: Town, 2: Werewolf, 3: Neutral
channel_name = "werewolves"
unique = False unique = False
game_start_message = ( game_start_message = (
"Your role is **Werewolf**\n" "Your role is **Werewolf**\n"
@ -16,34 +22,19 @@ class VanillaWerewolf(Role):
"Vote to kill players at night with `[p]ww vote <ID>`" "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): async def see_alignment(self, source=None):
""" """
Interaction for investigative roles attempting Interaction for investigative roles attempting
to see team (Village, Werewolf Other) to see team (Village, Werewolf Other)
""" """
return "Werewolf" return ALIGNMENT_WEREWOLF
async def get_role(self, source=None): async def get_role(self, source=None):
""" """
Interaction for powerful access of role Interaction for powerful access of role
Unlikely to be able to deceive this Unlikely to be able to deceive this
""" """
return "Werewolf" return "VanillaWerewolf"
async def see_role(self, source=None): async def see_role(self, source=None):
""" """
@ -52,10 +43,13 @@ class VanillaWerewolf(Role):
""" """
return "Werewolf" return "Werewolf"
async def _at_game_start(self, data=None): @wolflistener("at_game_start", priority=2)
if self.channel_id: async def _at_game_start(self):
print("Wolf has channel_id: " + self.channel_id) if self.channel_name:
await self.game.register_channel(self.channel_id, self, WolfVote) # Add VoteGroup WolfVote 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) 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): class Villager(Role):
rand_choice = True # 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)
category = [1] # List of enrolled categories (listed above) rand_choice = True
alignment = 1 # 1: Town, 2: Werewolf, 3: Neutral 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 channel_id = "" # Empty for no private channel
unique = False # Only one of this role per game unique = False # Only one of this role per game
game_start_message = ( game_start_message = (
@ -13,15 +20,12 @@ class Villager(Role):
"Lynch players during the day with `[p]ww vote <ID>`" "Lynch players during the day with `[p]ww vote <ID>`"
) )
def __init__(self, game):
super().__init__(game)
async def see_alignment(self, source=None): async def see_alignment(self, source=None):
""" """
Interaction for investigative roles attempting 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): 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 Base VoteGroup class for werewolf game
Handles secret channels and group decisions Handles secret channels and group decisions
@ -8,58 +15,41 @@ class VoteGroup:
channel_id = "" channel_id = ""
def __init__(self, game, channel): def __init__(self, game, channel):
super().__init__(game)
self.game = game self.game = game
self.channel = channel self.channel = channel
self.players = [] self.players = []
self.vote_results = {} self.vote_results = {}
self.properties = {} # Extra data for other options self.properties = {} # Extra data for other options
self.action_list = [ def __repr__(self):
(self._at_game_start, 1), # (Action, Priority) return f"{self.__class__.__name__}({self.channel},{self.players})"
(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", priority=1)
async def _at_game_start(self):
async def _at_game_start(self, data=None):
await self.channel.send(" ".join(player.mention for player in self.players)) await self.channel.send(" ".join(player.mention for player in self.players))
async def _at_day_start(self, data=None): @wolflistener("at_kill", priority=1)
pass async def _at_kill(self, player):
if player in self.players:
async def _at_voted(self, data=None): self.players.remove(player)
pass
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): @wolflistener("at_hang", priority=1)
pass async def _at_hang(self, player):
if player in self.players:
self.players.remove(player)
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: if self.channel is None:
return return
self.vote_results = {}
await self.game.generate_targets(self.channel) 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: if self.channel is None:
return return
@ -70,11 +60,8 @@ class VoteGroup:
target = max(set(vote_list), key=vote_list.count) target = max(set(vote_list), key=vote_list.count)
if target: if target:
# Do what you voted on # Do what the votegroup votes on
pass raise NotImplementedError
async def _at_visit(self, data=None):
pass
async def register_players(self, *players): async def register_players(self, *players):
""" """
@ -90,7 +77,7 @@ class VoteGroup:
self.players.remove(player) self.players.remove(player)
if not self.players: if not self.players:
# ToDo: Trigger deletion of votegroup # TODO: Confirm deletion
pass pass
async def vote(self, target, author, target_id): async def vote(self, target, author, target_id):

@ -0,0 +1 @@
from .wolfvote import WolfVote

@ -1,6 +1,12 @@
import logging
import random 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): class WolfVote(VoteGroup):
@ -13,71 +19,29 @@ class WolfVote(VoteGroup):
kill_messages = [ kill_messages = [
"**{ID}** - {target} was mauled by wolves", "**{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): def __init__(self, game, channel):
super().__init__(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.killer = None # Added killer
self.action_list = [ @wolflistener("at_night_start", priority=2)
(self._at_game_start, 1), # (Action, Priority) async def _at_night_start(self):
(self._at_day_start, 0), await super()._at_night_start()
(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
await self.game.generate_targets(self.channel)
mention_list = " ".join(player.mention for player in self.players) mention_list = " ".join(player.mention for player in self.players)
if mention_list != "": if mention_list != "":
await self.channel.send(mention_list) await self.channel.send(mention_list)
self.killer = random.choice(self.players) 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: if self.channel is None:
return return
@ -87,34 +51,23 @@ class WolfVote(VoteGroup):
if vote_list: if vote_list:
target_id = max(set(vote_list), key=vote_list.count) 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: if target_id is not None and self.killer:
await self.game.kill(target_id, self.killer, random.choice(self.kill_messages)) 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: else:
await self.channel.send("**No kill will be attempted tonight...**") 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): async def vote(self, target, author, target_id):
""" """
Receive vote from game 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 import discord
from redbot.core import Config, checks, commands from redbot.core import Config, checks, commands
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.commands import Cog from redbot.core.commands import Cog
from redbot.core.utils import AsyncIter
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu from redbot.core.utils.menus import DEFAULT_CONTROLS, menu
from .builder import ( from werewolf.builder import (
GameBuilder, GameBuilder,
role_from_alignment, role_from_alignment,
role_from_category, role_from_category,
role_from_id, role_from_id,
role_from_name, 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): class Werewolf(Cog):
@ -43,7 +57,7 @@ class Werewolf(Cog):
return return
def __unload(self): def __unload(self):
print("Unload called") log.debug("Unload called")
for game in self.games.values(): for game in self.games.values():
del game del game
@ -58,9 +72,9 @@ class Werewolf(Cog):
code = await gb.build_game(ctx) code = await gb.build_game(ctx)
if code != "": if code != "":
await ctx.send("Your game code is **{}**".format(code)) await ctx.maybe_send_embed(f"Your game code is **{code}**")
else: else:
await ctx.send("No code generated") await ctx.maybe_send_embed("No code generated")
@checks.guildowner() @checks.guildowner()
@commands.group() @commands.group()
@ -77,31 +91,36 @@ class Werewolf(Cog):
""" """
Lists current guild settings Lists current guild settings
""" """
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("Failed to get settings") # await ctx.send("Failed to get settings")
return None # 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="Role", value=str(role))
embed.add_field(name="Category", value=str(category)) embed.add_field(name="Category", value=str(category))
embed.add_field(name="Channel", value=str(channel)) embed.add_field(name="Channel", value=str(channel))
embed.add_field(name="Log Channel", value=str(log_channel)) embed.add_field(name="Log Channel", value=str(log_channel))
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.guild_only() @commands.guild_only()
@wwset.command(name="role") @wwset.command(name="role")
async def wwset_role(self, ctx: commands.Context, role: discord.Role = None): 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 This role should not be manually assigned
""" """
if role is None: if role is None:
await self.config.guild(ctx.guild).role_id.set(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: else:
await self.config.guild(ctx.guild).role_id.set(role.id) 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() @commands.guild_only()
@wwset.command(name="category") @wwset.command(name="category")
@ -111,14 +130,16 @@ class Werewolf(Cog):
""" """
if category_id is None: if category_id is None:
await self.config.guild(ctx.guild).category_id.set(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: else:
category = discord.utils.get(ctx.guild.categories, id=int(category_id)) category = discord.utils.get(ctx.guild.categories, id=int(category_id))
if category is None: if category is None:
await ctx.send("Category not found") await ctx.maybe_send_embed("Category not found")
return return
await self.config.guild(ctx.guild).category_id.set(category.id) 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() @commands.guild_only()
@wwset.command(name="channel") @wwset.command(name="channel")
@ -128,10 +149,12 @@ class Werewolf(Cog):
""" """
if channel is None: if channel is None:
await self.config.guild(ctx.guild).channel_id.set(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: else:
await self.config.guild(ctx.guild).channel_id.set(channel.id) 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() @commands.guild_only()
@wwset.command(name="logchannel") @wwset.command(name="logchannel")
@ -141,10 +164,12 @@ class Werewolf(Cog):
""" """
if channel is None: if channel is None:
await self.config.guild(ctx.guild).log_channel_id.set(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: else:
await self.config.guild(ctx.guild).log_channel_id.set(channel.id) 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() @commands.group()
async def ww(self, ctx: commands.Context): async def ww(self, ctx: commands.Context):
@ -162,9 +187,9 @@ class Werewolf(Cog):
""" """
game = await self._get_game(ctx, game_code) game = await self._get_game(ctx, game_code)
if not game: if not game:
await ctx.send("Failed to start a new game") await ctx.maybe_send_embed("Failed to start a new game")
else: 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() @commands.guild_only()
@ww.command(name="join") @ww.command(name="join")
@ -173,28 +198,49 @@ class Werewolf(Cog):
Joins a game of Werewolf Joins a game of Werewolf
""" """
game = await self._get_game(ctx) game: Game = await self._get_game(ctx)
if not game: 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 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() @commands.guild_only()
@ww.command(name="code") @ww.command(name="code")
async def ww_code(self, ctx: commands.Context, 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) game = await self._get_game(ctx)
if not game: 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 return
await game.set_code(ctx, code) await game.set_code(ctx, code)
await ctx.tick()
@commands.guild_only() @commands.guild_only()
@ww.command(name="quit") @ww.command(name="quit")
@ -206,6 +252,7 @@ class Werewolf(Cog):
game = await self._get_game(ctx) game = await self._get_game(ctx)
await game.quit(ctx.author, ctx.channel) await game.quit(ctx.author, ctx.channel)
await ctx.tick()
@commands.guild_only() @commands.guild_only()
@ww.command(name="start") @ww.command(name="start")
@ -215,10 +262,12 @@ class Werewolf(Cog):
""" """
game = await self._get_game(ctx) game = await self._get_game(ctx)
if not game: 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): if not await game.setup(ctx):
pass # Do something? pass # ToDo something?
await ctx.tick()
@commands.guild_only() @commands.guild_only()
@ww.command(name="stop") @ww.command(name="stop")
@ -226,17 +275,18 @@ class Werewolf(Cog):
""" """
Stops the current game Stops the current game
""" """
if ctx.guild is None: # if ctx.guild is None:
# Private message, can't get guild # # Private message, can't get guild
await ctx.send("Cannot start game from PM!") # await ctx.send("Cannot stop game from PM!")
return # return
if ctx.guild.id not in self.games or self.games[ctx.guild.id].game_over: 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 return
game = await self._get_game(ctx) game = await self._get_game(ctx)
game.game_over = True 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() @commands.guild_only()
@ww.command(name="vote") @ww.command(name="vote")
@ -250,7 +300,7 @@ class Werewolf(Cog):
target_id = None target_id = None
if target_id is 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 return
# if ctx.guild is None: # if ctx.guild is None:
@ -267,7 +317,7 @@ class Werewolf(Cog):
game = await self._get_game(ctx) game = await self._get_game(ctx)
if game is None: if game is None:
await ctx.send("No game running, cannot vote") await ctx.maybe_send_embed("No game running, cannot vote")
return return
# Game handles response now # Game handles response now
@ -277,7 +327,7 @@ class Werewolf(Cog):
elif channel in (c["channel"] for c in game.p_channels.values()): elif channel in (c["channel"] for c in game.p_channels.values()):
await game.vote(ctx.author, target_id, channel) await game.vote(ctx.author, target_id, channel)
else: 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") @ww.command(name="choose")
async def ww_choose(self, ctx: commands.Context, data): async def ww_choose(self, ctx: commands.Context, data):
@ -288,7 +338,7 @@ class Werewolf(Cog):
""" """
if ctx.guild is not None: 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 return
# DM nonsense, find their game # DM nonsense, find their game
# If multiple games, panic # If multiple games, panic
@ -296,7 +346,7 @@ class Werewolf(Cog):
if await game.get_player_by_member(ctx.author): if await game.get_player_by_member(ctx.author):
break # game = game break # game = game
else: 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 return
await game.choose(ctx, data) await game.choose(ctx, data)
@ -317,7 +367,7 @@ class Werewolf(Cog):
if from_name: if from_name:
await menu(ctx, from_name, DEFAULT_CONTROLS) await menu(ctx, from_name, DEFAULT_CONTROLS)
else: 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") @ww_search.command(name="alignment")
async def ww_search_alignment(self, ctx: commands.Context, alignment: int): async def ww_search_alignment(self, ctx: commands.Context, alignment: int):
@ -327,7 +377,7 @@ class Werewolf(Cog):
if from_alignment: if from_alignment:
await menu(ctx, from_alignment, DEFAULT_CONTROLS) await menu(ctx, from_alignment, DEFAULT_CONTROLS)
else: 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") @ww_search.command(name="category")
async def ww_search_category(self, ctx: commands.Context, category: int): async def ww_search_category(self, ctx: commands.Context, category: int):
@ -337,7 +387,7 @@ class Werewolf(Cog):
if pages: if pages:
await menu(ctx, pages, DEFAULT_CONTROLS) await menu(ctx, pages, DEFAULT_CONTROLS)
else: 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") @ww_search.command(name="index")
async def ww_search_index(self, ctx: commands.Context, idx: int): async def ww_search_index(self, ctx: commands.Context, idx: int):
@ -347,24 +397,32 @@ class Werewolf(Cog):
if idx_embed is not None: if idx_embed is not None:
await ctx.send(embed=idx_embed) await ctx.send(embed=idx_embed)
else: 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): async def _get_game(self, ctx: commands.Context, game_code=None) -> Union[Game, None]:
guild: discord.Guild = ctx.guild guild: discord.Guild = getattr(ctx, "guild", None)
if guild is None: if guild is None:
# Private message, can't get guild # 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 return None
if guild.id not in self.games or self.games[guild.id].game_over: 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...")
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") await ctx.maybe_send_embed("Cannot start a new game")
return None 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] return self.games[guild.id]
@ -385,23 +443,30 @@ class Werewolf(Cog):
if role_id is not None: if role_id is not None:
role = discord.utils.get(guild.roles, id=role_id) role = discord.utils.get(guild.roles, id=role_id)
if role is None: # if role is None:
await ctx.send("Game Role is invalid") # # await ctx.send("Game Role is invalid")
return False, None, None, None, None # return False, None, None, None, None
if category_id is not None: if category_id is not None:
category = discord.utils.get(guild.categories, id=category_id) category = discord.utils.get(guild.categories, id=category_id)
if category is None: # if category is None:
await ctx.send("Game Category is invalid") # # await ctx.send("Game Category is invalid")
return False, None, None, None, None # return False, role, None, None, None
if channel_id is not None: if channel_id is not None:
channel = discord.utils.get(guild.text_channels, id=channel_id) channel = discord.utils.get(guild.text_channels, id=channel_id)
if channel is None: # if channel is None:
await ctx.send("Village Channel is invalid") # # await ctx.send("Village Channel is invalid")
return False, None, None, None, None # return False, role, category, None, None
if log_channel_id is not None: if log_channel_id is not None:
log_channel = discord.utils.get(guild.text_channels, id=log_channel_id) log_channel = discord.utils.get(guild.text_channels, id=log_channel_id)
if log_channel is None: # if log_channel is None:
await ctx.send("Log Channel is invalid") # # await ctx.send("Log Channel is invalid")
return False, None, None, None, None # 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…
Cancel
Save