|
|
|
import bisect
|
|
|
|
from collections import defaultdict
|
|
|
|
from random import choice
|
|
|
|
|
|
|
|
import discord
|
|
|
|
from redbot.core import RedContext
|
|
|
|
|
|
|
|
# Import all roles here
|
|
|
|
from werewolf.roles.seer import Seer
|
|
|
|
from werewolf.roles.vanillawerewolf import VanillaWerewolf
|
|
|
|
from werewolf.roles.villager import Villager
|
|
|
|
from werewolf.utils.menus import menu, prev_page, next_page, close_menu
|
|
|
|
|
|
|
|
# All roles in this list for iterating
|
|
|
|
|
|
|
|
ROLE_LIST = sorted([Villager, Seer, VanillaWerewolf], key=lambda x: x.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]]
|
|
|
|
|
|
|
|
ROLE_PAGES = []
|
|
|
|
PAGE_GROUPS = []
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
return embed
|
|
|
|
|
|
|
|
|
|
|
|
def setup():
|
|
|
|
# Roles
|
|
|
|
if len(ROLE_PAGES) - 1 not in PAGE_GROUPS:
|
|
|
|
PAGE_GROUPS.append(len(ROLE_PAGES) - 1)
|
|
|
|
|
|
|
|
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
|
|
|
|
1 = VanillaWerewolf
|
|
|
|
T1 - T6 = Random Town (1: Random, 2: Investigative, 3: Protective, 4: Government,
|
|
|
|
5: Killing, 6: Power (Special night action))
|
|
|
|
W1, W2, W5, W6 = Random Werewolf
|
|
|
|
N1 = Benign Neutral
|
|
|
|
|
|
|
|
0001-1112T11W112N2
|
|
|
|
0,0,0,1,11,12,E1,R1,R1,R1,R2,P2
|
|
|
|
|
|
|
|
pre-letter = exact role position
|
|
|
|
double digit position preempted by `-`
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
async def parse_code(code, game):
|
|
|
|
"""Do the magic described above"""
|
|
|
|
decode = []
|
|
|
|
|
|
|
|
digits = 1
|
|
|
|
built = ""
|
|
|
|
category = ""
|
|
|
|
for c in code:
|
|
|
|
if built == "T" or built == "W" or built == "N":
|
|
|
|
# Random Towns
|
|
|
|
category = built
|
|
|
|
built = ""
|
|
|
|
digits = 1
|
|
|
|
elif built == "-":
|
|
|
|
digits += 1
|
|
|
|
|
|
|
|
if len(built) < digits:
|
|
|
|
built += c
|
|
|
|
|
|
|
|
try:
|
|
|
|
idx = int(built)
|
|
|
|
except ValueError:
|
|
|
|
raise ValueError("Invalid code")
|
|
|
|
|
|
|
|
if category == "": # no randomness yet
|
|
|
|
decode.append(ROLE_LIST[idx](game))
|
|
|
|
else:
|
|
|
|
options = []
|
|
|
|
if category == "T":
|
|
|
|
options = [role for role in ROLE_LIST if idx in role.category]
|
|
|
|
elif category == "W":
|
|
|
|
options = [role for role in ROLE_LIST if 10 + idx in role.category]
|
|
|
|
elif category == "N":
|
|
|
|
options = [role for role in ROLE_LIST if 20 + idx in role.category]
|
|
|
|
pass
|
|
|
|
|
|
|
|
if not options:
|
|
|
|
raise ValueError("No Match Found")
|
|
|
|
|
|
|
|
decode.append(choice(options)(game))
|
|
|
|
|
|
|
|
return decode
|
|
|
|
|
|
|
|
|
|
|
|
async def encode(roles, rand_roles):
|
|
|
|
"""Convert role list to code"""
|
|
|
|
out_code = ""
|
|
|
|
|
|
|
|
digit_sort = sorted(role for role in roles if role < 10)
|
|
|
|
for role in digit_sort:
|
|
|
|
out_code += str(role)
|
|
|
|
|
|
|
|
digit_sort = sorted(role for role in roles if 10 <= role < 100)
|
|
|
|
if digit_sort:
|
|
|
|
out_code += "-"
|
|
|
|
for role in digit_sort:
|
|
|
|
out_code += str(role)
|
|
|
|
# That covers up to 99 roles, add another set here if we breach 100
|
|
|
|
|
|
|
|
if rand_roles:
|
|
|
|
# town sort
|
|
|
|
digit_sort = sorted(role for role in rand_roles if role <= 6)
|
|
|
|
if digit_sort:
|
|
|
|
out_code += "T"
|
|
|
|
for role in digit_sort:
|
|
|
|
out_code += role
|
|
|
|
|
|
|
|
# werewolf sort
|
|
|
|
digit_sort = sorted(role for role in rand_roles if 10 < role <= 20)
|
|
|
|
if digit_sort:
|
|
|
|
out_code += "W"
|
|
|
|
for role in digit_sort:
|
|
|
|
out_code += role
|
|
|
|
|
|
|
|
# neutral sort
|
|
|
|
digit_sort = sorted(role for role in rand_roles if 20 < role <= 30)
|
|
|
|
if digit_sort:
|
|
|
|
out_code += "N"
|
|
|
|
for role in digit_sort:
|
|
|
|
out_code += role
|
|
|
|
|
|
|
|
return out_code
|
|
|
|
|
|
|
|
|
|
|
|
async def next_group(ctx: RedContext, 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: RedContext, 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 say_role_list(code_list):
|
|
|
|
roles = [ROLE_LIST[idx] for idx in code_list]
|
|
|
|
embed = discord.Embed(title="Currently selected roles")
|
|
|
|
role_dict = defaultdict(int)
|
|
|
|
for role in roles:
|
|
|
|
role_dict[str(role.__name__)] += 1
|
|
|
|
|
|
|
|
for k, v in role_dict.items():
|
|
|
|
embed.add_field(name=k, value="Count: {}".format(v), inline=True)
|
|
|
|
|
|
|
|
return embed
|
|
|
|
|
|
|
|
|
|
|
|
class GameBuilder:
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
self.code = []
|
|
|
|
self.rand_roles = []
|
|
|
|
setup()
|
|
|
|
|
|
|
|
async def build_game(self, ctx: RedContext):
|
|
|
|
new_controls = {
|
|
|
|
'⏪': prev_group,
|
|
|
|
"⬅": prev_page,
|
|
|
|
'☑': self.select_page,
|
|
|
|
"➡": next_page,
|
|
|
|
'⏩': next_group,
|
|
|
|
'📇': self.list_roles,
|
|
|
|
"❌": close_menu
|
|
|
|
}
|
|
|
|
|
|
|
|
await ctx.send("Browse through roles and add the ones you want using the check mark")
|
|
|
|
|
|
|
|
await menu(ctx, ROLE_PAGES, new_controls, timeout=60)
|
|
|
|
|
|
|
|
out = await encode(self.code, self.rand_roles)
|
|
|
|
return out
|
|
|
|
|
|
|
|
async def list_roles(self, ctx: RedContext, 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
|
|
|
|
|
|
|
|
await ctx.send(embed=say_role_list(self.code))
|
|
|
|
|
|
|
|
return await menu(ctx, pages, controls, message=message,
|
|
|
|
page=page, timeout=timeout)
|
|
|
|
|
|
|
|
async def select_page(self, ctx: RedContext, 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
|
|
|
|
|
|
|
|
if page >= len(ROLE_LIST):
|
|
|
|
self.rand_roles.append(CATEGORY_COUNT[len(ROLE_LIST) - page])
|
|
|
|
else:
|
|
|
|
self.code.append(page)
|
|
|
|
|
|
|
|
return await menu(ctx, pages, controls, message=message,
|
|
|
|
page=page, timeout=timeout)
|