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 import discord
from redbot.core import RedContext
# Import all roles here # Import all roles here
from werewolf.role import Role
from werewolf.roles.seer import Seer from werewolf.roles.seer import Seer
from werewolf.roles.vanillawerewolf import VanillaWerewolf from werewolf.roles.vanillawerewolf import VanillaWerewolf
from werewolf.roles.villager import Villager 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 # 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: Example code:
0 = Villager 0 = Villager
1 = VanillaWerewolf 1 = VanillaWerewolf
E1 = Random Town T1 - T6 = Random Town (1: Random, 2: Investigative, 3: Protective, 4: Government,
R1 = Random Werewolf 5: Killing, 6: Power (Special night action))
J1 = Benign Neutral 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 0,0,0,1,11,12,E1,R1,R1,R1,R2,P2
pre-letter = exact role position pre-letter = exact role position
@ -29,27 +101,191 @@ double digit position preempted by `-`
async def parse_code(code, game): async def parse_code(code, game):
"""Do the magic described above""" """Do the magic described above"""
out: List[Role] = [] decode = []
decode = code.copy() # for now, pass exact names
for role_id in decode: digits = 1
print(role_id) built = ""
if role_id == "Villager": category = ""
role = Villager(game) for c in code:
elif role_id == "VanillaWerewolf": if built == "T" or built == "W" or built == "N":
role = VanillaWerewolf(game) # Random Towns
elif role_id == "Seer": category = built
role = Seer(game) built = ""
else: # Fail to parse digits = 1
return None elif built == "-":
out.append(role) digits += 1
return out if len(built) < digits:
built += c
continue
async def build_game(channel: discord.TextChannel):
await channel.send("Not currently available") try:
idx = int(built)
code = 12345678 except ValueError:
raise ValueError("Invalid code")
await channel.send("Your game code is **`{}`**".format(code))
# Make this embeds 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 from typing import List
import discord import discord
from redbot.core import RedContext
from werewolf.builder import parse_code from werewolf.builder import parse_code
from werewolf.player import Player from werewolf.player import Player
from werewolf.role import Role
class Game: class Game:
""" """
Base class to run a single game of Werewolf 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 = { default_secret_channel = {
"channel": None, "channel": None,
@ -33,11 +29,11 @@ class Game:
def __init__(self, guild: discord.Guild, role: discord.Role, game_code=None): def __init__(self, guild: discord.Guild, role: discord.Role, game_code=None):
self.guild = guild self.guild = guild
self.game_code = ["Seer", "VanillaWerewolf", "Villager"] self.game_code = game_code
self.game_role = role self.game_role = role
self.roles = [] self.roles = [] # List[Role]
self.players = [] self.players = [] # List[Player]
self.day_vote = {} # author: target self.day_vote = {} # author: target
self.vote_totals = {} # id: total_votes self.vote_totals = {} # id: total_votes
@ -51,8 +47,8 @@ class Game:
self.day_count = 0 self.day_count = 0
self.ongoing_vote = False self.ongoing_vote = False
self.channel_category = None self.channel_category = None # discord.CategoryChannel
self.village_channel = None self.village_channel = None # discord.TextChannel
self.p_channels = {} # uses default_secret_channel self.p_channels = {} # uses default_secret_channel
self.vote_groups = {} # ID : VoteGroup() self.vote_groups = {} # ID : VoteGroup()
@ -76,7 +72,7 @@ class Game:
for c_data in self.p_channels.values(): for c_data in self.p_channels.values():
asyncio.ensure_future(c_data["channel"].delete("Werewolf game-over")) 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 Runs the initial setup
@ -87,10 +83,13 @@ class Game:
4. Start game 4. Start game
""" """
if self.game_code: if self.game_code:
await self.get_roles() await self.get_roles(ctx)
if len(self.players) != len(self.roles): 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 = [] self.roles = []
return False return False
@ -256,7 +255,7 @@ class Game:
reaction_list = message.reactions reaction_list = message.reactions
up_votes = sum(p for p in reaction_list if p.emoji == "👍" and not p.me) 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: if down_votes > up_votes:
embed = discord.Embed(title="Vote Results", color=0xff0000) embed = discord.Embed(title="Vote Results", color=0xff0000)
@ -616,14 +615,21 @@ class Game:
async def get_day_target(self, target_id, source=None): async def get_day_target(self, target_id, source=None):
return self.players[target_id] # ToDo check source 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: if game_code is not None:
self.game_code = game_code self.game_code = game_code
if self.game_code is None: if self.game_code is None:
return False 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: if not self.roles:
return False 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 import discord
from discord.ext import commands from discord.ext import commands
from redbot.core import Config from redbot.core import Config
from redbot.core import RedContext from redbot.core import RedContext
from redbot.core.bot import Red from redbot.core.bot import Red
from werewolf.builder import GameBuilder
from werewolf.game import Game from werewolf.game import Game
@ -13,7 +12,6 @@ class Werewolf:
""" """
Base to host werewolf on a guild Base to host werewolf on a guild
""" """
games: Dict[int, Game]
def __init__(self, bot: Red): def __init__(self, bot: Red):
self.bot = bot self.bot = bot
@ -33,6 +31,16 @@ class Werewolf:
for game in self.games.values(): for game in self.games.values():
del game 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() @commands.group()
async def wwset(self, ctx: RedContext): async def wwset(self, ctx: RedContext):
""" """

Loading…
Cancel
Save