builder mostly done
This commit is contained in:
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)
|
||||
decode = []
|
||||
|
||||
return out
|
||||
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 build_game(channel: discord.TextChannel):
|
||||
await channel.send("Not currently available")
|
||||
async def encode(roles, rand_roles):
|
||||
"""Convert role list to code"""
|
||||
out_code = ""
|
||||
|
||||
code = 12345678
|
||||
digit_sort = sorted(role for role in roles if role < 10)
|
||||
for role in digit_sort:
|
||||
out_code += str(role)
|
||||
|
||||
await channel.send("Your game code is **`{}`**".format(code))
|
||||
# Make this embeds
|
||||
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
|
||||
|
134
werewolf/utils/menus.py
Normal file
134
werewolf/utils/menus.py
Normal file
@ -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…
x
Reference in New Issue
Block a user