Merge branch 'werewolf_develop'

pull/5/head
Bobloy 7 years ago
commit d62e01b785

@ -1,21 +1,94 @@
import bisect
from collections import defaultdict
from random import choice
import discord import discord
from discord.ext import commands
# Import all roles here # Import all roles here
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 redbot.core.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 = [0]
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
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
@ -23,29 +96,225 @@ double digit position preempted by `-`
""" """
async def parse_code(code): async def parse_code(code, game):
"""Do the magic described above""" """Do the magic described above"""
out = [] 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 for c in code:
elif role_id == "VanillaWerewolf": if built == "T" or built == "W" or built == "N":
role = VanillaWerewolf # Random Towns
elif role_id == "Seer": category = built
role = Seer 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 IndexError("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 += str(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 += str(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 += str(role)
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):
return [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):
return [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):
try:
role = ROLE_LIST[idx]
except IndexError:
return None
return role_embed(idx, role, ALIGNMENT_COLORS[role.alignment - 1])
def role_from_name(name: str):
return [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):
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 role in rand_roles:
if 0 < role <= 6:
role_dict["Town {}".format(ROLE_CATEGORIES[role])] += 1
if 10 < role <= 16:
role_dict["Werewolf {}".format(ROLE_CATEGORIES[role])] += 1
if 20 < role <= 26:
role_dict["Neutral {}".format(ROLE_CATEGORIES[role])] += 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: commands.Context):
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: 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
await ctx.send(embed=say_role_list(self.code, self.rand_roles))
return await menu(ctx, pages, controls, message=message,
page=page, timeout=timeout)
async def select_page(self, 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
if page >= len(ROLE_LIST):
self.rand_roles.append(CATEGORY_COUNT[page-len(ROLE_LIST)])
else:
self.code.append(page)
return await menu(ctx, pages, controls, message=message,
page=page, timeout=timeout)

@ -2,6 +2,7 @@ import asyncio
import random import random
import discord import discord
from discord.ext import commands
from werewolf.builder import parse_code from werewolf.builder import parse_code
from werewolf.player import Player from werewolf.player import Player
@ -25,19 +26,14 @@ class Game:
day_vote_count = 3 day_vote_count = 3
# def __new__(cls, guild, game_code): def __init__(self, guild: discord.Guild, role: discord.Role = None,
# game_code = ["VanillaWerewolf", "Villager", "Villager"] category: discord.CategoryChannel = None, village: discord.TextChannel = None,
# log_channel: discord.TextChannel = None, game_code=None):
# return super().__new__(cls, guild, game_code)
def __init__(self, guild, role, game_code):
self.guild = guild self.guild = guild
self.game_code = ["VanillaWerewolf"] self.game_code = game_code
self.game_role = role
self.roles = [] self.roles = [] # List[Role]
self.players = [] # List[Player]
self.players = []
self.day_vote = {} # author: target self.day_vote = {} # author: target
self.vote_totals = {} # id: total_votes self.vote_totals = {} # id: total_votes
@ -49,9 +45,15 @@ class Game:
self.day_time = False self.day_time = False
self.day_count = 0 self.day_count = 0
self.ongoing_vote = False
self.game_role = role # discord.Role
self.channel_category = category # discord.CategoryChannel
self.village_channel = village # discord.TextChannel
self.log_channel = log_channel
self.channel_category = None self.to_delete = set()
self.village_channel = None self.save_perms = {}
self.p_channels = {} # uses default_secret_channel self.p_channels = {} # uses default_secret_channel
self.vote_groups = {} # ID : VoteGroup() self.vote_groups = {} # ID : VoteGroup()
@ -60,22 +62,22 @@ class Game:
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
def __del__(self): # def __del__(self):
""" # """
Cleanup channels as necessary # Cleanup channels as necessary
:return: # :return:
""" # """
#
print("Delete is called") # print("Delete is called")
#
self.game_over = True # self.game_over = True
if self.village_channel: # if self.village_channel:
asyncio.ensure_future(self.village_channel.delete("Werewolf game-over")) # asyncio.ensure_future(self.village_channel.delete("Werewolf game-over"))
#
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: commands.Context):
""" """
Runs the initial setup Runs the initial setup
@ -86,17 +88,34 @@ 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
if self.game_role is None: if self.game_role is None:
await ctx.send("Game role not configured, cannot start") try:
self.roles = [] self.game_role = await ctx.guild.create_role(name="WW Players",
return False hoist=True,
mentionable=True,
reason="(BOT) Werewolf game role")
self.to_delete.add(self.game_role)
except (discord.Forbidden, discord.HTTPException):
await ctx.send("Game role not configured and unable to generate one, cannot start")
self.roles = []
return False
try:
for player in self.players:
await player.member.add_roles(*[self.game_role])
except discord.Forbidden:
await ctx.send(
"Unable to add role **{}**\nBot is missing `manage_roles` permissions".format(self.game_role.name))
return False
await self.assign_roles() await self.assign_roles()
@ -104,21 +123,55 @@ class Game:
overwrite = { overwrite = {
self.guild.default_role: discord.PermissionOverwrite(read_messages=True, send_messages=False, self.guild.default_role: discord.PermissionOverwrite(read_messages=True, send_messages=False,
add_reactions=False), add_reactions=False),
self.guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True, add_reactions=True), self.guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True, add_reactions=True,
manage_messages=True, manage_channels=True,
manage_roles=True),
self.game_role: discord.PermissionOverwrite(read_messages=True, send_messages=True) self.game_role: discord.PermissionOverwrite(read_messages=True, send_messages=True)
} }
if self.channel_category is None:
self.channel_category = await self.guild.create_category("Werewolf Game",
overwrites=overwrite,
reason="(BOT) New game of werewolf")
else: # No need to modify categories
pass
# await self.channel_category.edit(name="🔴 Werewolf Game (ACTIVE)", reason="(BOT) New game of werewolf")
# for target, ow in overwrite.items():
# await self.channel_category.set_permissions(target=target,
# overwrite=ow,
# reason="(BOT) New game of werewolf")
if self.village_channel is None:
try:
self.village_channel = await self.guild.create_text_channel("🔵Werewolf",
overwrites=overwrite,
reason="(BOT) New game of werewolf",
category=self.channel_category)
except discord.Forbidden:
await ctx.send("Unable to create Game Channel and none was provided\n"
"Grant Bot appropriate permissions or assign a game_channel")
return False
else:
self.save_perms[self.village_channel] = self.village_channel.overwrites
try:
await self.village_channel.edit(name="🔵Werewolf",
category=self.channel_category,
reason="(BOT) New game of werewolf")
except discord.Forbidden as e:
print("Unable to rename Game Channel")
print(e)
await ctx.send("Unable to rename Game Channel, ignoring")
self.channel_category = await self.guild.create_category("ww-game", overwrites=overwrite, reason="New game of " try:
"werewolf") for target, ow in overwrite.items():
curr = self.village_channel.overwrites_for(target)
# for player in self.players: curr.update(**{perm: value for perm, value in ow})
# overwrite[player.member] = discord.PermissionOverwrite(read_messages=True) await self.village_channel.set_permissions(target=target,
overwrite=curr,
self.village_channel = await self.guild.create_text_channel("Village Square", reason="(BOT) New game of werewolf")
overwrites=overwrite, except discord.Forbidden:
reason="New game of werewolf", await ctx.send("Unable to edit Game Channel permissions\n"
category=self.channel_category) "Grant Bot appropriate permissions to manage permissions")
return
self.started = True
# Assuming everything worked so far # Assuming everything worked so far
print("Pre at_game_start") print("Pre at_game_start")
await self._at_game_start() # This will queue channels and votegroups to be made await self._at_game_start() # This will queue channels and votegroups to be made
@ -127,7 +180,9 @@ class Game:
print("Channel id: " + channel_id) print("Channel id: " + channel_id)
overwrite = { overwrite = {
self.guild.default_role: discord.PermissionOverwrite(read_messages=False), self.guild.default_role: discord.PermissionOverwrite(read_messages=False),
self.guild.me: discord.PermissionOverwrite(read_messages=True) self.guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True, add_reactions=True,
manage_messages=True, manage_channels=True,
manage_roles=True)
} }
for player in self.p_channels[channel_id]["players"]: for player in self.p_channels[channel_id]["players"]:
@ -135,7 +190,7 @@ class Game:
channel = await self.guild.create_text_channel(channel_id, channel = await self.guild.create_text_channel(channel_id,
overwrites=overwrite, overwrites=overwrite,
reason="Ww game secret channel", reason="(BOT) WW game secret channel",
category=self.channel_category) category=self.channel_category)
self.p_channels[channel_id]["channel"] = channel self.p_channels[channel_id]["channel"] = channel
@ -207,13 +262,15 @@ class Game:
return return
self.can_vote = True self.can_vote = True
await asyncio.sleep(12) # 4 minute days FixMe to 120 later await asyncio.sleep(24) # 4 minute days FixMe to 120 later
if check(): if check():
return return
await self.village_channel.send(embed=discord.Embed(title="**Two minutes of daylight remain...**")) await self.village_channel.send(embed=discord.Embed(title="**Two minutes of daylight remain...**"))
await asyncio.sleep(12) # 4 minute days FixMe to 120 later await asyncio.sleep(24) # 4 minute days FixMe to 120 later
# Need a loop here to wait for trial to end (can_vote?) # Need a loop here to wait for trial to end (can_vote?)
while self.ongoing_vote:
asyncio.sleep(5)
if check(): if check():
return return
@ -226,16 +283,17 @@ class Game:
data = {"player": target} data = {"player": target}
await self._notify(2, data) await self._notify(2, data)
self.ongoing_vote = True
self.used_votes += 1 self.used_votes += 1
self.can_vote = False await self.speech_perms(self.village_channel, target.member) # Only target can talk
await self.speech_perms(self.village_channel, target.member)
await self.village_channel.send( await self.village_channel.send(
"**{} will be put to trial and has 30 seconds to defend themselves**".format(target.mention)) "**{} will be put to trial and has 30 seconds to defend themselves**".format(target.mention))
await asyncio.sleep(30) await asyncio.sleep(30)
await self.speech_perms(self.village_channel, target.member, undo=True) await self.speech_perms(self.village_channel, target.member, undo=True) # No one can talk
message = await self.village_channel.send( message = await self.village_channel.send(
"Everyone will now vote whether to lynch {}\n" "Everyone will now vote whether to lynch {}\n"
@ -243,42 +301,46 @@ class Game:
"*Majority rules, no-lynch on ties, " "*Majority rules, no-lynch on ties, "
"vote both or neither to abstain, 15 seconds to vote*".format(target.mention)) "vote both or neither to abstain, 15 seconds to vote*".format(target.mention))
await self.village_channel.add_reaction("👍") await message.add_reaction("👍")
await self.village_channel.add_reaction("👎") await message.add_reaction("👎")
await asyncio.sleep(15) await asyncio.sleep(15)
reaction_list = message.reactions reaction_list = message.reactions
up_votes = sum(p.emoji == "👍" and not p.me for p in reaction_list) up_votes = sum(p for p in reaction_list if p.emoji == "👍" and not p.me)
down_votes = sum(p.emoji == "👎" and not p.me for p in reaction_list) down_votes = sum(p for p in reaction_list if p.emoji == "👎" and not p.me)
if len(down_votes) > len(up_votes): if down_votes > up_votes:
embed = discord.Embed(title="Vote Results", color=0xff0000) embed = discord.Embed(title="Vote Results", color=0xff0000)
else: else:
embed = discord.Embed(title="Vote Results", color=0x80ff80) embed = discord.Embed(title="Vote Results", color=0x80ff80)
embed.add_field(name="👎", value="**{}**".format(len(up_votes)), inline=True) embed.add_field(name="👎", value="**{}**".format(up_votes), inline=True)
embed.add_field(name="👍", value="**{}**".format(len(down_votes)), inline=True) embed.add_field(name="👍", value="**{}**".format(down_votes), inline=True)
await self.village_channel.send(embed=embed) await self.village_channel.send(embed=embed)
if len(down_votes) > len(up_votes): if down_votes > up_votes:
await self.village_channel.send("**Voted to lynch {}!**".format(target.mention)) await self.village_channel.send("**Voted to lynch {}!**".format(target.mention))
await self.lynch(target) await self.lynch(target)
self.can_vote = False
else: else:
await self.village_channel.send("**{} has been spared!**".format(target.mention)) await self.village_channel.send("**{} has been spared!**".format(target.mention))
if self.used_votes >= self.day_vote_count: if self.used_votes >= self.day_vote_count:
await self.village_channel.send("**All votes have been used! Day is now over!**") await self.village_channel.send("**All votes have been used! Day is now over!**")
self.can_vote = False
else: else:
await self.village_channel.send( await self.village_channel.send(
"**{}**/**{}** of today's votes have been used!\n" "**{}**/**{}** of today's votes have been used!\n"
"Nominate carefully..".format(self.used_votes, self.day_vote_count)) "Nominate carefully..".format(self.used_votes, self.day_vote_count))
self.can_vote = True # Only re-enable voting if more votes remain
self.ongoing_vote = False
if not self.can_vote: if not self.can_vote:
await self._at_day_end() await self._at_day_end()
else:
await self.normal_perms(self.village_channel) # No point if about to be night
async def _at_kill(self, target): # ID 3 async def _at_kill(self, target): # ID 3
if self.game_over: if self.game_over:
@ -329,7 +391,7 @@ class Game:
return return
await self._notify(7) await self._notify(7)
await asyncio.sleep(15) await asyncio.sleep(10)
await self._at_day_start() await self._at_day_start()
async def _at_visit(self, target, source): # ID 8 async def _at_visit(self, target, source): # ID 8
@ -355,16 +417,22 @@ class Game:
############END Notify structure############ ############END Notify structure############
async def generate_targets(self, channel): async def generate_targets(self, channel, with_roles=False):
embed = discord.Embed(title="Remaining Players") embed = discord.Embed(title="Remaining Players")
for i in range(len(self.players)): for i in range(len(self.players)):
player = self.players[i] player = self.players[i]
if player.alive: if player.alive:
status = "" status = ""
else: else:
status = "*Dead*" status = "*[Dead]*-"
embed.add_field(name="ID# **{}**".format(i), if with_roles or not player.alive:
value="{} {}".format(status, player.member.display_name), inline=True) embed.add_field(name="ID# **{}**".format(i),
value="{}{}-{}".format(status, player.member.display_name, str(player.role)),
inline=True)
else:
embed.add_field(name="ID# **{}**".format(i),
value="{}{}".format(status, player.member.display_name),
inline=True)
return await channel.send(embed=embed) return await channel.send(embed=embed)
@ -400,6 +468,13 @@ class Game:
self.players.append(Player(member)) self.players.append(Player(member))
if self.game_role is not None:
try:
await member.add_roles(*[self.game_role])
except discord.Forbidden:
await channel.send(
"Unable to add role **{}**\nBot is missing `manage_roles` permissions".format(self.game_role.name))
await channel.send("{} has been added to the game, " await channel.send("{} has been added to the game, "
"total players is **{}**".format(member.mention, len(self.players))) "total players is **{}**".format(member.mention, len(self.players)))
@ -417,6 +492,7 @@ class Game:
await channel.send("{} has left the game".format(member.mention)) await channel.send("{} has left the game".format(member.mention))
else: else:
self.players = [player for player in self.players if player.member != member] self.players = [player for player in self.players if player.member != member]
await member.remove_roles(*[self.game_role])
await channel.send("{} chickened out, player count is now **{}**".format(member.mention, len(self.players))) await channel.send("{} chickened out, player count is now **{}**".format(member.mention, len(self.players)))
async def choose(self, ctx, data): async def choose(self, ctx, data):
@ -431,7 +507,7 @@ class Game:
return return
if not player.alive: if not player.alive:
await ctx.send("**Corpses** can't vote...") await ctx.send("**Corpses** can't participate...")
return return
if player.role.blocked: if player.role.blocked:
@ -441,7 +517,7 @@ class Game:
# Let role do target validation, might be alternate targets # Let role do target validation, might be alternate targets
# I.E. Go on alert? y/n # I.E. Go on alert? y/n
await player.choose(ctx, data) await player.role.choose(ctx, data)
async def _visit(self, target, source): async def _visit(self, target, source):
await target.role.visit(source) await target.role.visit(source)
@ -471,7 +547,7 @@ class Game:
return return
if not player.alive: if not player.alive:
await channel.send("Corpses can't vote") await channel.send("Corpses can't vote...")
return return
if channel == self.village_channel: if channel == self.village_channel:
@ -531,7 +607,9 @@ class Game:
out = "**{ID}** - " + method out = "**{ID}** - " + method
return out.format(ID=target.id, target=target.member.display_name) return out.format(ID=target.id, target=target.member.display_name)
else: else:
return "**{ID}** - {target} was found dead".format(ID=target.id, target=target.member.display_name) return "**{ID}** - {target} the {role} was found dead".format(ID=target.id,
target=target.member.display_name,
role=await target.role.get_role())
async def _quit(self, player): async def _quit(self, player):
""" """
@ -595,14 +673,25 @@ 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 set_code(self, ctx: commands.Context, game_code):
if game_code is not None:
self.game_code = game_code
await ctx.send("Code has been set")
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) try:
self.roles = await parse_code(self.game_code, self)
except ValueError as e:
await ctx.send("Invalid Code: Code contains unknown character\n{}".format(e))
return False
except IndexError as e:
await ctx.send("Invalid Code: Code references unknown role\n{}".format(e))
if not self.roles: if not self.roles:
return False return False
@ -613,11 +702,10 @@ class Game:
self.players.sort(key=lambda pl: pl.member.display_name.lower()) self.players.sort(key=lambda pl: pl.member.display_name.lower())
if len(self.roles) != len(self.players): if len(self.roles) != len(self.players):
await self.village_channel("Unhandled error - roles!=players") await self.village_channel.send("Unhandled error - roles!=players")
return False return False
for idx, role in enumerate(self.roles): for idx, role in enumerate(self.roles):
self.roles[idx] = role(self)
await self.roles[idx].assign_player(self.players[idx]) await self.roles[idx].assign_player(self.players[idx])
# Sorted players, now assign id's # Sorted players, now assign id's
await self.players[idx].assign_id(idx) await self.players[idx].assign_id(idx)
@ -645,28 +733,67 @@ class Game:
await channel.set_permissions(self.game_role, read_messages=True, send_messages=False) await channel.set_permissions(self.game_role, read_messages=True, send_messages=False)
await channel.set_permissions(member, send_messages=True) await channel.set_permissions(member, send_messages=True)
async def normal_perms(self, channel, member_list): async def normal_perms(self, channel):
await channel.set_permissions(self.game_role, read_messages=True, send_messages=True) await channel.set_permissions(self.game_role, read_messages=True, send_messages=True)
# for member in member_list:
# await channel.set_permissions(member, read_messages=True)
async def _check_game_over(self): async def _check_game_over(self):
alive_players = [player for player self.players if player.alive] # return # ToDo: re-enable game-over checking
alive_players = [player for player in self.players if player.alive]
if len(alive_players)<=2:
if len(alive_players) <= 0:
await self.village_channel.send(embed=discord.Embed(title="**Everyone is dead! Game Over!**"))
self.game_over = True
elif len(alive_players) == 1:
self.game_over = True
await self._announce_winners(alive_players)
elif len(alive_players) == 2:
# Check 1v1 victory conditions ToDo # Check 1v1 victory conditions ToDo
pass self.game_over = True
alignment1 = alive_players[0].role.alignment
alignment2 = alive_players[1].role.alignment
if alignment1 == alignment2: # Same team
winners = alive_players
else:
winners = [max(alive_players, key=lambda p: p.role.alignment)]
await self._announce_winners(winners)
else: else:
#Check if everyone is on the same team # Check if everyone is on the same team
alignment = alive_players[0].role.alignment alignment = alive_players[0].role.alignment # Get first allignment and compare to rest
for player in alive_players: for player in alive_players:
if player.role.alignment != alignment: if player.role.alignment != alignment:
return False return
# Only remaining team wins # Only remaining team wins
self.game_over = True
await self._announce_winners(alive_players)
# If no return, cleanup and end game
await self._end_game()
async def _announce_winners(self, winnerlist):
await self.village_channel.send(self.game_role.mention)
embed = discord.Embed(title='Game Over', description='The Following Players have won!')
for player in winnerlist:
embed.add_field(name=player.member.display_name, value=str(player.role), inline=True)
embed.set_thumbnail(url='https://emojipedia-us.s3.amazonaws.com/thumbs/160/twitter/134/trophy_1f3c6.png')
await self.village_channel.send(embed=embed)
await self.generate_targets(self.village_channel, True)
async def _end_game(self): async def _end_game(self):
# ToDo # Remove game_role access for potential archiving for now
pass reason = '(BOT) End of WW game'
for obj in self.to_delete:
print(obj)
await obj.delete(reason=reason)
try:
await self.village_channel.edit(reason=reason, name="Werewolf")
for target, overwrites in self.save_perms[self.village_channel]:
await self.village_channel.set_permissions(target, overwrite=overwrites, reason=reason)
await self.village_channel.set_permissions(self.game_role, overwrite=None, reason=reason)
except (discord.HTTPException, discord.NotFound, discord.errors.NotFound):
pass
# Optional dynamic channels/categories

@ -46,6 +46,12 @@ class Role:
"You win by testing the game\n" "You win by testing the game\n"
"Lynch players during the day with `[p]ww vote <ID>`" "Lynch players during the day with `[p]ww vote <ID>`"
) )
description = (
"This is the basic role\n"
"All roles are based on this Class"
"Has no special significance"
)
icon_url = None # Adding a URL here will enable a thumbnail of the role
def __init__(self, game): def __init__(self, game):
self.game = game self.game = game
@ -65,6 +71,9 @@ class Role:
(self._at_visit, 0) (self._at_visit, 0)
] ]
def __repr__(self):
return self.__class__.__name__
async def on_event(self, event, data): async def on_event(self, event, data):
""" """
See Game class for event guide See Game class for event guide

@ -2,7 +2,7 @@ from werewolf.role import Role
class Seer(Role): class Seer(Role):
rand_choice = False # Determines if it can be picked as a random role (False for unusually disruptive roles) rand_choice = True # Determines if it can be picked as a random role (False for unusually disruptive roles)
category = [1, 2] # List of enrolled categories (listed above) category = [1, 2] # List of enrolled categories (listed above)
alignment = 1 # 1: Town, 2: Werewolf, 3: Neutral alignment = 1 # 1: Town, 2: Werewolf, 3: Neutral
channel_id = "" # Empty for no private channel channel_id = "" # Empty for no private channel
@ -29,7 +29,8 @@ class Seer(Role):
(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)
] ]
# async def on_event(self, event, data): # async def on_event(self, event, data):
@ -96,15 +97,22 @@ class Seer(Role):
# pass # pass
async def _at_night_start(self, data=None): async def _at_night_start(self, data=None):
if not self.player.alive:
return
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("{}\n**Pick a target to see tonight**\n") await self.player.send_dm("**Pick a target to see tonight**\n")
async def _at_night_end(self, data=None): async def _at_night_end(self, data=None):
target = await self.game.visit(self.see_target) if self.see_target is None:
if self.player.alive:
await self.player.send_dm("You will not use your powers tonight...")
return
target = await self.game.visit(self.see_target, self.player)
alignment = None alignment = None
if target: if target:
alignment = await target.see_alignment(self.player) alignment = await target.role.see_alignment(self.player)
if alignment == "Werewolf": if alignment == "Werewolf":
out = "Your insight reveals this player to be a **Werewolf!**" out = "Your insight reveals this player to be a **Werewolf!**"
@ -133,6 +141,10 @@ class Seer(Role):
async def choose(self, ctx, data): async def choose(self, ctx, data):
"""Handle night actions""" """Handle night actions"""
if not self.player.alive: # FixMe: Game handles this?
await self.player.send_dm("You're already dead!")
return
target_id = int(data) target_id = int(data)
try: try:
target = self.game.players[target_id] target = self.game.players[target_id]

@ -2,7 +2,7 @@ from werewolf.role import Role
class Villager(Role): class Villager(Role):
rand_choice = False # Determines if it can be picked as a random role (False for unusually disruptive roles) rand_choice = True # Determines if it can be picked as a random role (False for unusually disruptive roles)
category = [1] # List of enrolled categories (listed above) category = [1] # List of enrolled categories (listed above)
alignment = 1 # 1: Town, 2: Werewolf, 3: Neutral alignment = 1 # 1: Town, 2: Werewolf, 3: Neutral
channel_id = "" # Empty for no private channel channel_id = "" # Empty for no private channel

@ -1,9 +1,12 @@
import discord import discord
from discord.ext import commands from discord.ext import commands
from redbot.core import Config from redbot.core import Config, checks
from redbot.core import RedContext
from redbot.core.bot import Red
from werewolf.builder import GameBuilder, role_from_name, role_from_alignment, role_from_category, role_from_id
from werewolf.game import Game from werewolf.game import Game
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
class Werewolf: class Werewolf:
@ -11,12 +14,15 @@ class Werewolf:
Base to host werewolf on a guild Base to host werewolf on a guild
""" """
def __init__(self, bot): def __init__(self, bot: Red):
self.bot = bot self.bot = bot
self.config = Config.get_conf(self, identifier=87101114101119111108102, force_registration=True) self.config = Config.get_conf(self, identifier=87101114101119111108102, force_registration=True)
default_global = {} default_global = {}
default_guild = { default_guild = {
"role": None "role_id": None,
"category_id": None,
"channel_id": None,
"log_channel_id": None
} }
self.config.register_global(**default_global) self.config.register_global(**default_global)
@ -29,26 +35,102 @@ 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: commands.Context):
gb = GameBuilder()
code = await gb.build_game(ctx)
if code != "":
await ctx.send("Your game code is **{}**".format(code))
else:
await ctx.send("No code generated")
@checks.guildowner()
@commands.group() @commands.group()
async def wwset(self, ctx: RedContext): async def wwset(self, ctx: commands.Context):
""" """
Base command to adjust settings. Check help for command list. Base command to adjust settings. Check help for command list.
""" """
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send_help() await ctx.send_help()
@commands.guild_only()
@wwset.command(name="list")
async def wwset_list(self, ctx: commands.Context):
"""
Lists current guild settings
"""
success, role, category, channel, log_channel = await self._get_settings(ctx)
if not success:
await ctx.send("Failed to get settings")
return None
embed = discord.Embed(title="Current Guild Settings")
embed.add_field(name="Role", value=str(role))
embed.add_field(name="Category", value=str(category))
embed.add_field(name="Channel", value=str(channel))
embed.add_field(name="Log Channel", value=str(log_channel))
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, role: discord.Role): async def wwset_role(self, ctx: commands.Context, role: discord.Role=None):
""" """
Assign the game role Assign the game role
This role should not be manually assigned This role should not be manually assigned
""" """
await self.config.guild(ctx.guild).role.set(role.id) if role is None:
await ctx.send("Game role has been set to **{}**".format(role.name)) await self.config.guild(ctx.guild).role_id.set(None)
await ctx.send("Cleared Game Role")
else:
await self.config.guild(ctx.guild).role_id.set(role.id)
await ctx.send("Game Role has been set to **{}**".format(role.name))
@commands.guild_only()
@wwset.command(name="category")
async def wwset_category(self, ctx: commands.Context, category_id=None):
"""
Assign the channel category
"""
if category_id is None:
await self.config.guild(ctx.guild).category_id.set(None)
await ctx.send("Cleared Game Channel Category")
else:
category = discord.utils.get(ctx.guild.categories, id=int(category_id))
if category is None:
await ctx.send("Category not found")
return
await self.config.guild(ctx.guild).category_id.set(category.id)
await ctx.send("Game Channel Category has been set to **{}**".format(category.name))
@commands.guild_only()
@wwset.command(name="channel")
async def wwset_channel(self, ctx: commands.Context, channel: discord.TextChannel=None):
"""
Assign the village channel
"""
if channel is None:
await self.config.guild(ctx.guild).channel_id.set(None)
await ctx.send("Cleared Game Channel")
else:
await self.config.guild(ctx.guild).channel_id.set(channel.id)
await ctx.send("Game Channel has been set to **{}**".format(channel.mention))
@commands.guild_only()
@wwset.command(name="logchannel")
async def wwset_log_channel(self, ctx: commands.Context, channel: discord.TextChannel=None):
"""
Assign the log channel
"""
if channel is None:
await self.config.guild(ctx.guild).log_channel_id.set(None)
await ctx.send("Cleared Game Log Channel")
else:
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))
@commands.group() @commands.group()
async def ww(self, ctx: RedContext): async def ww(self, ctx: commands.Context):
""" """
Base command for this cog. Check help for the commands list. Base command for this cog. Check help for the commands list.
""" """
@ -56,27 +138,25 @@ class Werewolf:
await ctx.send_help() await ctx.send_help()
@commands.guild_only() @commands.guild_only()
@ww.command() @ww.command(name="new")
async def new(self, ctx, game_code): async def ww_new(self, ctx: commands.Context, game_code=None):
""" """
Create and join a new game of Werewolf Create and join a new game of Werewolf
""" """
game = await self._get_game(ctx, game_code)
game = await self._get_game(ctx.guild, game_code)
if not game: if not game:
await ctx.send("Failed to start a new game") await ctx.send("Failed to start a new game")
else: else:
await ctx.send("New game has started") await ctx.send("Game is ready to join! Use `[p]ww join`")
@commands.guild_only() @commands.guild_only()
@ww.command() @ww.command(name="join")
async def join(self, ctx): async def ww_join(self, ctx: commands.Context):
""" """
Joins a game of Werewolf Joins a game of Werewolf
""" """
game = await self._get_game(ctx.guild) 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.send("No game to join!\nCreate a new one with `[p]ww new`")
@ -85,49 +165,71 @@ class Werewolf:
await game.join(ctx.author, ctx.channel) await game.join(ctx.author, ctx.channel)
@commands.guild_only() @commands.guild_only()
@ww.command() @ww.command(name="code")
async def quit(self, ctx): async def ww_code(self, ctx: commands.Context, code):
"""
Adjust game code
"""
game = await self._get_game(ctx)
if not game:
await ctx.send("No game to join!\nCreate a new one with `[p]ww new`")
return
await game.set_code(ctx, code)
@commands.guild_only()
@ww.command(name="quit")
async def ww_quit(self, ctx: commands.Context):
""" """
Quit a game of Werewolf Quit a game of Werewolf
""" """
game = await self._get_game(ctx.guild) game = await self._get_game(ctx)
await game.quit(ctx.author, ctx.channel) await game.quit(ctx.author, ctx.channel)
@commands.guild_only() @commands.guild_only()
@ww.command() @ww.command(name="start")
async def start(self, ctx): async def ww_start(self, ctx: commands.Context):
""" """
Checks number of players and attempts to start the game Checks number of players and attempts to start the game
""" """
game = await self._get_game(ctx.guild) game = await self._get_game(ctx)
if not game: if not game:
await ctx.send("No game running, cannot start") await ctx.send("No game running, cannot start")
await game.setup(ctx) if not await game.setup(ctx):
pass # Do something?
@commands.guild_only() @commands.guild_only()
@ww.command() @ww.command(name="stop")
async def stop(self, ctx): async def ww_stop(self, ctx: commands.Context):
""" """
Stops the current game Stops the current game
""" """
game = await self._get_game(ctx.guild) if ctx.guild is None:
if not game: # Private message, can't get guild
await ctx.send("No game running, cannot stop") await ctx.send("Cannot start game from PM!")
return
if ctx.guild.id not in self.games or self.games[ctx.guild.id].game_over:
await ctx.send("No game to stop")
return
game = await self._get_game(ctx)
game.game_over = True game.game_over = True
await ctx.send("Game has been stopped")
@commands.guild_only() @commands.guild_only()
@ww.command() @ww.command(name="vote")
async def vote(self, ctx, target_id: int): async def ww_vote(self, ctx: commands.Context, target_id: int):
""" """
Vote for a player by ID Vote for a player by ID
""" """
try: try:
target_id = int(target_id) target_id = int(target_id)
except: except ValueError:
target_id = None target_id = None
if target_id is None: if target_id is None:
@ -145,7 +247,7 @@ class Werewolf:
# return # return
# else: # else:
game = await self._get_game(ctx.guild) game = await self._get_game(ctx)
if game is None: if game is None:
await ctx.send("No game running, cannot vote") await ctx.send("No game running, cannot vote")
@ -160,8 +262,8 @@ class Werewolf:
else: else:
await ctx.send("Nothing to vote for in this channel") await ctx.send("Nothing to vote for in this channel")
@ww.command() @ww.command(name="choose")
async def choose(self, ctx, data): async def ww_choose(self, ctx: commands.Context, data):
""" """
Arbitrary decision making Arbitrary decision making
Handled by game+role Handled by game+role
@ -171,7 +273,6 @@ class Werewolf:
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.send("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
for game in self.games.values(): for game in self.games.values():
@ -183,20 +284,108 @@ class Werewolf:
await game.choose(ctx, data) await game.choose(ctx, data)
async def _get_game(self, guild, game_code=None): @ww.group(name="search")
async def ww_search(self, ctx: commands.Context):
"""
Find custom roles by name, alignment, category, or ID
"""
if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self.ww_search:
await ctx.send_help()
@ww_search.command(name="name")
async def ww_search_name(self, ctx: commands.Context, *, name):
"""Search for a role by name"""
if name is not None:
from_name = role_from_name(name)
if from_name:
await menu(ctx, from_name, DEFAULT_CONTROLS)
else:
await ctx.send("No roles containing that name were found")
@ww_search.command(name="alignment")
async def ww_search_alignment(self, ctx: commands.Context, alignment: int):
"""Search for a role by alignment"""
if alignment is not None:
from_alignment = role_from_alignment(alignment)
if from_alignment:
await menu(ctx, from_alignment, DEFAULT_CONTROLS)
else:
await ctx.send("No roles with that alignment were found")
@ww_search.command(name="category")
async def ww_search_category(self, ctx: commands.Context, category: int):
"""Search for a role by category"""
if category is not None:
pages = role_from_category(category)
if pages:
await menu(ctx, pages, DEFAULT_CONTROLS)
else:
await ctx.send("No roles in that category were found")
@ww_search.command(name="index")
async def ww_search_index(self, ctx: commands.Context, idx: int):
"""Search for a role by ID"""
if idx is not None:
idx_embed = role_from_id(idx)
if idx_embed is not None:
await ctx.send(embed=idx_embed)
else:
await ctx.send("Role ID not found")
async def _get_game(self, ctx: commands.Context, game_code=None):
guild: discord.Guild = ctx.guild
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!")
return None return None
if guild.id not in self.games: if guild.id not in self.games or self.games[guild.id].game_over:
if not game_code: await ctx.send("Starting a new game...")
return None success, role, category, channel, log_channel = await self._get_settings(ctx)
role = await self.config.guild(guild).role()
role = discord.utils.get(guild.roles, id=role) if not success:
if role is None: await ctx.send("Cannot start a new game")
return None return None
self.games[guild.id] = Game(guild, role, game_code)
self.games[guild.id] = Game(guild, role, category, channel, log_channel, game_code)
return self.games[guild.id] return self.games[guild.id]
async def _game_start(self, game): async def _game_start(self, game):
await game.start() await game.start()
async def _get_settings(self, ctx):
guild = ctx.guild
role = None
category = None
channel = None
log_channel = None
role_id = await self.config.guild(guild).role_id()
category_id = await self.config.guild(guild).category_id()
channel_id = await self.config.guild(guild).channel_id()
log_channel_id = await self.config.guild(guild).log_channel_id()
if role_id is not None:
role = discord.utils.get(guild.roles, id=role_id)
if role is None:
await ctx.send("Game Role is invalid")
return False, None, None, None, None
if category_id is not None:
category = discord.utils.get(guild.categories, id=category_id)
if category is None:
await ctx.send("Game Category is invalid")
return False, None, None, None, None
if channel_id is not None:
channel = discord.utils.get(guild.text_channels, id=channel_id)
if channel is None:
await ctx.send("Village Channel is invalid")
return False, None, None, None, None
if log_channel_id is not None:
log_channel = discord.utils.get(guild.text_channels, id=log_channel_id)
if log_channel is None:
await ctx.send("Log Channel is invalid")
return False, None, None, None, None
return True, role, category, channel, log_channel

Loading…
Cancel
Save