builder mostly done

pull/5/head
bobloy 7 years ago
parent d0facafb92
commit d73c452015

@ -1,25 +1,97 @@
from typing import List
import bisect
from collections import defaultdict
from random import choice
import discord
from redbot.core import RedContext
# Import all roles here
from werewolf.role import Role
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 = [Villager, VanillaWerewolf]
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
E1 = Random Town
R1 = Random Werewolf
J1 = Benign Neutral
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-1112E11R112P2
0001-1112T11W112N2
0,0,0,1,11,12,E1,R1,R1,R1,R2,P2
pre-letter = exact role position
@ -29,27 +101,191 @@ double digit position preempted by `-`
async def parse_code(code, game):
"""Do the magic described above"""
out: List[Role] = []
decode = code.copy() # for now, pass exact names
for role_id in decode:
print(role_id)
if role_id == "Villager":
role = Villager(game)
elif role_id == "VanillaWerewolf":
role = VanillaWerewolf(game)
elif role_id == "Seer":
role = Seer(game)
else: # Fail to parse
return None
out.append(role)
return out
async def build_game(channel: discord.TextChannel):
await channel.send("Not currently available")
code = 12345678
await channel.send("Your game code is **`{}`**".format(code))
# Make this embeds
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
continue
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)

@ -3,20 +3,16 @@ import random
from typing import List
import discord
from redbot.core import RedContext
from werewolf.builder import parse_code
from werewolf.player import Player
from werewolf.role import Role
class Game:
"""
Base class to run a single game of Werewolf
"""
players: List[Player]
roles: List[Role]
channel_category: discord.CategoryChannel
village_channel: discord.TextChannel
default_secret_channel = {
"channel": None,
@ -33,11 +29,11 @@ class Game:
def __init__(self, guild: discord.Guild, role: discord.Role, game_code=None):
self.guild = guild
self.game_code = ["Seer", "VanillaWerewolf", "Villager"]
self.game_code = game_code
self.game_role = role
self.roles = []
self.players = []
self.roles = [] # List[Role]
self.players = [] # List[Player]
self.day_vote = {} # author: target
self.vote_totals = {} # id: total_votes
@ -51,8 +47,8 @@ class Game:
self.day_count = 0
self.ongoing_vote = False
self.channel_category = None
self.village_channel = None
self.channel_category = None # discord.CategoryChannel
self.village_channel = None # discord.TextChannel
self.p_channels = {} # uses default_secret_channel
self.vote_groups = {} # ID : VoteGroup()
@ -76,7 +72,7 @@ class Game:
for c_data in self.p_channels.values():
asyncio.ensure_future(c_data["channel"].delete("Werewolf game-over"))
async def setup(self, ctx):
async def setup(self, ctx: RedContext):
"""
Runs the initial setup
@ -87,10 +83,13 @@ class Game:
4. Start game
"""
if self.game_code:
await self.get_roles()
await self.get_roles(ctx)
if len(self.players) != len(self.roles):
await ctx.send("Player count does not match role count, cannot start")
await ctx.send("Player count does not match role count, cannot start\n"
"Currently **{} / {}**\n"
"Use `{}ww code` to pick a new game"
"".format(len(self.players), len(self.roles), ctx.prefix))
self.roles = []
return False
@ -256,7 +255,7 @@ class Game:
reaction_list = message.reactions
up_votes = sum(p for p in reaction_list if p.emoji == "👍" and not p.me)
down_votes = sum(p for p in reaction_list if p.emoji == "👎" and not p.me )
down_votes = sum(p for p in reaction_list if p.emoji == "👎" and not p.me)
if down_votes > up_votes:
embed = discord.Embed(title="Vote Results", color=0xff0000)
@ -616,14 +615,21 @@ class Game:
async def get_day_target(self, target_id, source=None):
return self.players[target_id] # ToDo check source
async def get_roles(self, game_code=None):
async def get_roles(self, ctx, game_code=None):
if game_code is not None:
self.game_code = game_code
if self.game_code is None:
return False
self.roles = await parse_code(self.game_code, self)
try:
self.roles = await parse_code(self.game_code, self)
except ValueError("Invalid Code"):
await ctx.send("Invalid Code")
return False
except ValueError("No Match Found"):
await ctx.send("Code contains unknown role")
return False
if not self.roles:
return False

@ -0,0 +1,134 @@
import asyncio
import discord
from redbot.core import RedContext
async def menu(ctx: RedContext, pages: list,
controls: dict,
message: discord.Message = None, page: int = 0,
timeout: float = 30.0):
"""
An emoji-based menu
.. note:: All pages should be of the same type
.. note:: All functions for handling what a particular emoji does
should be coroutines (i.e. :code:`async def`). Additionally,
they must take all of the parameters of this function, in
addition to a string representing the emoji reacted with.
This parameter should be the last one, and none of the
parameters in the handling functions are optional
Parameters
----------
ctx: RedContext
The command context
pages: `list` of `str` or `discord.Embed`
The pages of the menu.
controls: dict
A mapping of emoji to the function which handles the action for the
emoji.
message: discord.Message
The message representing the menu. Usually :code:`None` when first opening
the menu
page: int
The current page number of the menu
timeout: float
The time (in seconds) to wait for a reaction
Raises
------
RuntimeError
If either of the notes above are violated
"""
if not all(isinstance(x, discord.Embed) for x in pages) and \
not all(isinstance(x, str) for x in pages):
raise RuntimeError("All pages must be of the same type")
for key, value in controls.items():
if not asyncio.iscoroutinefunction(value):
raise RuntimeError("Function must be a coroutine")
current_page = pages[page]
if not message:
if isinstance(current_page, discord.Embed):
message = await ctx.send(embed=current_page)
else:
message = await ctx.send(current_page)
for key in controls.keys():
await message.add_reaction(key)
else:
if isinstance(current_page, discord.Embed):
await message.edit(embed=current_page)
else:
await message.edit(content=current_page)
def react_check(r, u):
return u == ctx.author and str(r.emoji) in controls.keys()
try:
react, user = await ctx.bot.wait_for(
"reaction_add",
check=react_check,
timeout=timeout
)
except asyncio.TimeoutError:
try:
await message.clear_reactions()
except discord.Forbidden: # cannot remove all reactions
for key in controls.keys():
await message.remove_reaction(key, ctx.bot.user)
return None
return await controls[react.emoji](ctx, pages, controls,
message, page,
timeout, react.emoji)
async def next_page(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(pages) - 1:
next_page = 0 # Loop around to the first item
else:
next_page = page + 1
return await menu(ctx, pages, controls, message=message,
page=next_page, timeout=timeout)
async def prev_page(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 == 0:
page = len(pages) - 1 # Loop around to the last item
else:
page = page - 1
return await menu(ctx, pages, controls, message=message,
page=page, timeout=timeout)
async def close_menu(ctx: RedContext, pages: list,
controls: dict, message: discord.Message, page: int,
timeout: float, emoji: str):
if message:
await message.delete()
return None
DEFAULT_CONTROLS = {
"": next_page,
"": prev_page,
"": close_menu,
}

@ -1,11 +1,10 @@
from typing import Dict
import discord
from discord.ext import commands
from redbot.core import Config
from redbot.core import RedContext
from redbot.core.bot import Red
from werewolf.builder import GameBuilder
from werewolf.game import Game
@ -13,7 +12,6 @@ class Werewolf:
"""
Base to host werewolf on a guild
"""
games: Dict[int, Game]
def __init__(self, bot: Red):
self.bot = bot
@ -33,6 +31,16 @@ class Werewolf:
for game in self.games.values():
del game
@commands.command()
async def buildgame(self, ctx):
gb = GameBuilder()
code = await gb.build_game(ctx)
if code is not None:
await ctx.send("Your game code is **{}**".format(code))
else:
await ctx.send("No code generated")
@commands.group()
async def wwset(self, ctx: RedContext):
"""

Loading…
Cancel
Save