From 22aaa497f8d381631b982107d39410076a52e980 Mon Sep 17 00:00:00 2001 From: Bobloy Date: Mon, 23 Apr 2018 10:41:49 -0400 Subject: [PATCH 01/14] formatting --- werewolf/game.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index 3a559ca..c133dc7 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -651,13 +651,13 @@ class Game: # await channel.set_permissions(member, read_messages=True) async def _check_game_over(self): - alive_players = [player for player self.players if player.alive] + alive_players = [player for player in self.players if player.alive] - if len(alive_players)<=2: + if len(alive_players) <= 2: # Check 1v1 victory conditions ToDo pass else: - #Check if everyone is on the same team + # Check if everyone is on the same team alignment = alive_players[0].role.alignment for player in alive_players: if player.role.alignment != alignment: @@ -665,8 +665,6 @@ class Game: # Only remaining team wins - - async def _end_game(self): # ToDo pass From bae13550370a485daac9762dd28a128954096e01 Mon Sep 17 00:00:00 2001 From: Bobloy Date: Mon, 23 Apr 2018 11:55:24 -0400 Subject: [PATCH 02/14] end_game --- werewolf/game.py | 35 +++++++++++++++++++++++++++++------ werewolf/role.py | 3 +++ werewolf/werewolf.py | 24 ++++++++++++++---------- 3 files changed, 46 insertions(+), 16 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index c133dc7..979fda6 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -11,6 +11,7 @@ class Game: """ Base class to run a single game of Werewolf """ + village_channel: discord.TextChannel default_secret_channel = { "channel": None, @@ -30,7 +31,7 @@ class Game: # # return super().__new__(cls, guild, game_code) - def __init__(self, guild, role, game_code): + def __init__(self, guild: discord.Guild, role: discord.Role, game_code): self.guild = guild self.game_code = ["VanillaWerewolf"] self.game_role = role @@ -355,7 +356,7 @@ class Game: ############END Notify structure############ - async def generate_targets(self, channel): + async def generate_targets(self, channel, with_roles = False): embed = discord.Embed(title="Remaining Players") for i in range(len(self.players)): player = self.players[i] @@ -363,7 +364,11 @@ class Game: status = "" else: status = "*Dead*" - embed.add_field(name="ID# **{}**".format(i), + if with_roles: + 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) @@ -654,17 +659,35 @@ class Game: alive_players = [player for player in self.players if player.alive] if len(alive_players) <= 2: + self.game_over = True # Check 1v1 victory conditions ToDo pass else: # 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: if player.role.alignment != alignment: - return False + return # 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): - # ToDo + # Remove game_role access for potential archiving for now + await self.village_channel.set_permissions(self.game_role, overwrite=None) pass diff --git a/werewolf/role.py b/werewolf/role.py index 64c78a4..afede03 100644 --- a/werewolf/role.py +++ b/werewolf/role.py @@ -65,6 +65,9 @@ class Role: (self._at_visit, 0) ] + def __repr__(self): + return self.__class__.__name__ + async def on_event(self, event, data): """ See Game class for event guide diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index c5bf57e..ca03198 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -1,3 +1,5 @@ +from typing import Dict + import discord from discord.ext import commands from redbot.core import Config @@ -10,6 +12,7 @@ class Werewolf: """ Base to host werewolf on a guild """ + games: Dict[int, Game] def __init__(self, bot): self.bot = bot @@ -57,17 +60,17 @@ class Werewolf: @commands.guild_only() @ww.command() - async def new(self, ctx, game_code): + async def new(self, ctx, game_code=None): """ Create and join a new game of Werewolf """ - game = await self._get_game(ctx.guild, game_code) + game = await self._get_game(ctx, game_code) if not game: await ctx.send("Failed to start a new game") else: - await ctx.send("New game has started") + await ctx.send("Game is ready to join! Use `[p]`ww join`") @commands.guild_only() @ww.command() @@ -183,20 +186,21 @@ class Werewolf: await game.choose(ctx, data) - async def _get_game(self, guild, game_code=None): - if guild is None: + async def _get_game(self, ctx, game_code=None): + if ctx.guild is None: # Private message, can't get guild return None - if guild.id not in self.games: + if ctx.guild.id not in self.games or self.games[ctx.guild.id].game_over: + await ctx.send("Starting a new game...") if not game_code: return None - role = await self.config.guild(guild).role() - role = discord.utils.get(guild.roles, id=role) + role = await self.config.guild(ctx.guild).role() + role = discord.utils.get(ctx.guild.roles, id=role) if role is None: return None - self.games[guild.id] = Game(guild, role, game_code) + self.games[ctx.guild.id] = Game(ctx.guild, role, game_code) - return self.games[guild.id] + return self.games[ctx.guild.id] async def _game_start(self, game): await game.start() From d0facafb9260e32c0d75b1ab0758a076bb93421f Mon Sep 17 00:00:00 2001 From: Bobloy Date: Mon, 23 Apr 2018 17:05:58 -0400 Subject: [PATCH 03/14] Seer and voting fixes --- werewolf/builder.py | 14 +++-- werewolf/game.py | 124 +++++++++++++++++++++++++---------------- werewolf/roles/seer.py | 20 +++++-- werewolf/werewolf.py | 33 ++++++----- 4 files changed, 117 insertions(+), 74 deletions(-) diff --git a/werewolf/builder.py b/werewolf/builder.py index d14b25e..0be61d9 100644 --- a/werewolf/builder.py +++ b/werewolf/builder.py @@ -1,5 +1,9 @@ +from typing import List + import discord + # 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 @@ -23,18 +27,18 @@ double digit position preempted by `-` """ -async def parse_code(code): +async def parse_code(code, game): """Do the magic described above""" - out = [] + 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 + role = Villager(game) elif role_id == "VanillaWerewolf": - role = VanillaWerewolf + role = VanillaWerewolf(game) elif role_id == "Seer": - role = Seer + role = Seer(game) else: # Fail to parse return None out.append(role) diff --git a/werewolf/game.py b/werewolf/game.py index 979fda6..be7d0e1 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -1,16 +1,21 @@ import asyncio import random +from typing import List import discord 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 = { @@ -26,18 +31,12 @@ class Game: day_vote_count = 3 - # def __new__(cls, guild, game_code): - # game_code = ["VanillaWerewolf", "Villager", "Villager"] - # - # return super().__new__(cls, guild, game_code) - - def __init__(self, guild: discord.Guild, role: discord.Role, game_code): + def __init__(self, guild: discord.Guild, role: discord.Role, game_code=None): self.guild = guild - self.game_code = ["VanillaWerewolf"] + self.game_code = ["Seer", "VanillaWerewolf", "Villager"] self.game_role = role self.roles = [] - self.players = [] self.day_vote = {} # author: target @@ -50,6 +49,7 @@ class Game: self.day_time = False self.day_count = 0 + self.ongoing_vote = False self.channel_category = None self.village_channel = None @@ -99,6 +99,7 @@ class Game: self.roles = [] return False + self.started = True await self.assign_roles() # Create category and channel with individual overwrites @@ -109,15 +110,16 @@ class Game: self.game_role: discord.PermissionOverwrite(read_messages=True, send_messages=True) } - self.channel_category = await self.guild.create_category("ww-game", overwrites=overwrite, reason="New game of " - "werewolf") + self.channel_category = await self.guild.create_category("ww-game", + overwrites=overwrite, + reason="(BOT) New game of werewolf") # for player in self.players: # overwrite[player.member] = discord.PermissionOverwrite(read_messages=True) self.village_channel = await self.guild.create_text_channel("Village Square", overwrites=overwrite, - reason="New game of werewolf", + reason="(BOT) New game of werewolf", category=self.channel_category) # Assuming everything worked so far @@ -136,7 +138,7 @@ class Game: channel = await self.guild.create_text_channel(channel_id, overwrites=overwrite, - reason="Ww game secret channel", + reason="(BOT) WW game secret channel", category=self.channel_category) self.p_channels[channel_id]["channel"] = channel @@ -208,13 +210,15 @@ class Game: return 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(): return 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?) + while self.ongoing_vote: + asyncio.sleep(5) if check(): return @@ -227,16 +231,17 @@ class Game: data = {"player": target} await self._notify(2, data) + self.ongoing_vote = True + self.used_votes += 1 - self.can_vote = False - await self.speech_perms(self.village_channel, target.member) + await self.speech_perms(self.village_channel, target.member) # Only target can talk await self.village_channel.send( "**{} will be put to trial and has 30 seconds to defend themselves**".format(target.mention)) 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( "Everyone will now vote whether to lynch {}\n" @@ -244,42 +249,46 @@ class Game: "*Majority rules, no-lynch on ties, " "vote both or neither to abstain, 15 seconds to vote*".format(target.mention)) - await self.village_channel.add_reaction("πŸ‘") - await self.village_channel.add_reaction("πŸ‘Ž") + await message.add_reaction("πŸ‘") + await message.add_reaction("πŸ‘Ž") await asyncio.sleep(15) - reaction_list = message.reactions - up_votes = sum(p.emoji == "πŸ‘" and not p.me for p in reaction_list) - down_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 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) else: 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(len(down_votes)), inline=True) + embed.add_field(name="πŸ‘Ž", value="**{}**".format(up_votes), inline=True) + embed.add_field(name="πŸ‘", value="**{}**".format(down_votes), inline=True) 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.lynch(target) + self.can_vote = False else: await self.village_channel.send("**{} has been spared!**".format(target.mention)) if self.used_votes >= self.day_vote_count: await self.village_channel.send("**All votes have been used! Day is now over!**") + self.can_vote = False else: await self.village_channel.send( "**{}**/**{}** of today's votes have been used!\n" "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: 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 if self.game_over: @@ -330,7 +339,7 @@ class Game: return await self._notify(7) - await asyncio.sleep(15) + await asyncio.sleep(10) await self._at_day_start() async def _at_visit(self, target, source): # ID 8 @@ -356,20 +365,22 @@ class Game: ############END Notify structure############ - async def generate_targets(self, channel, with_roles = False): + async def generate_targets(self, channel, with_roles=False): embed = discord.Embed(title="Remaining Players") for i in range(len(self.players)): player = self.players[i] if player.alive: status = "" else: - status = "*Dead*" - if with_roles: + status = "*[Dead]*-" + if with_roles or not player.alive: embed.add_field(name="ID# **{}**".format(i), - value="{} {} {}".format(status, player.member.display_name, str(player.role)), inline=True) + 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) + value="{}{}".format(status, player.member.display_name), + inline=True) return await channel.send(embed=embed) @@ -405,6 +416,8 @@ class Game: self.players.append(Player(member)) + await member.add_roles(*[self.game_role]) + await channel.send("{} has been added to the game, " "total players is **{}**".format(member.mention, len(self.players))) @@ -422,6 +435,7 @@ class Game: await channel.send("{} has left the game".format(member.mention)) else: 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))) async def choose(self, ctx, data): @@ -436,7 +450,7 @@ class Game: return if not player.alive: - await ctx.send("**Corpses** can't vote...") + await ctx.send("**Corpses** can't participate...") return if player.role.blocked: @@ -446,7 +460,7 @@ class Game: # Let role do target validation, might be alternate targets # I.E. Go on alert? y/n - await player.choose(ctx, data) + await player.role.choose(ctx, data) async def _visit(self, target, source): await target.role.visit(source) @@ -476,7 +490,7 @@ class Game: return if not player.alive: - await channel.send("Corpses can't vote") + await channel.send("Corpses can't vote...") return if channel == self.village_channel: @@ -536,7 +550,9 @@ class Game: out = "**{ID}** - " + method return out.format(ID=target.id, target=target.member.display_name) 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): """ @@ -607,7 +623,7 @@ class Game: if self.game_code is None: return False - self.roles = await parse_code(self.game_code) + self.roles = await parse_code(self.game_code, self) if not self.roles: return False @@ -618,11 +634,10 @@ class Game: self.players.sort(key=lambda pl: pl.member.display_name.lower()) 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 for idx, role in enumerate(self.roles): - self.roles[idx] = role(self) await self.roles[idx].assign_player(self.players[idx]) # Sorted players, now assign id's await self.players[idx].assign_id(idx) @@ -650,18 +665,30 @@ class Game: await channel.set_permissions(self.game_role, read_messages=True, send_messages=False) 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) - # for member in member_list: - # await channel.set_permissions(member, read_messages=True) async def _check_game_over(self): + # 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 - 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: # Check if everyone is on the same team alignment = alive_players[0].role.alignment # Get first allignment and compare to rest @@ -686,8 +713,9 @@ class Game: await self.generate_targets(self.village_channel, True) - async def _end_game(self): # Remove game_role access for potential archiving for now - await self.village_channel.set_permissions(self.game_role, overwrite=None) - pass + reason = '(BOT) End of WW game' + await self.village_channel.set_permissions(self.game_role, overwrite=None, reason=reason) + await self.channel_category.set_permissions(self.game_role, overwrite=None, reason=reason) + await self.channel_category.edit(reason=reason, name="Archived Game") diff --git a/werewolf/roles/seer.py b/werewolf/roles/seer.py index ccd61be..7b3fe5d 100644 --- a/werewolf/roles/seer.py +++ b/werewolf/roles/seer.py @@ -29,7 +29,8 @@ class Seer(Role): (self._at_hang, 0), (self._at_day_end, 0), (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): @@ -96,15 +97,22 @@ class Seer(Role): # pass 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.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): - 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 if target: - alignment = await target.see_alignment(self.player) + alignment = await target.role.see_alignment(self.player) if alignment == "Werewolf": out = "Your insight reveals this player to be a **Werewolf!**" @@ -133,6 +141,10 @@ class Seer(Role): async def choose(self, ctx, data): """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) try: target = self.game.players[target_id] diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index ca03198..4e31746 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -4,6 +4,7 @@ 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.game import Game @@ -14,12 +15,12 @@ class Werewolf: """ games: Dict[int, Game] - def __init__(self, bot): + def __init__(self, bot: Red): self.bot = bot self.config = Config.get_conf(self, identifier=87101114101119111108102, force_registration=True) default_global = {} default_guild = { - "role": None + "role_id": None } self.config.register_global(**default_global) @@ -47,7 +48,7 @@ class Werewolf: Assign the game role This role should not be manually assigned """ - await self.config.guild(ctx.guild).role.set(role.id) + await self.config.guild(ctx.guild).role_id.set(role.id) await ctx.send("Game role has been set to **{}**".format(role.name)) @commands.group() @@ -64,13 +65,11 @@ class Werewolf: """ Create and join a new game of Werewolf """ - game = await self._get_game(ctx, game_code) - if not game: await ctx.send("Failed to start a new game") else: - await ctx.send("Game is ready to join! Use `[p]`ww join`") + await ctx.send("Game is ready to join! Use `[p]ww join`") @commands.guild_only() @ww.command() @@ -79,7 +78,7 @@ class Werewolf: Joins a game of Werewolf """ - game = await self._get_game(ctx.guild) + game = await self._get_game(ctx) if not game: await ctx.send("No game to join!\nCreate a new one with `[p]ww new`") @@ -94,7 +93,7 @@ class 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) @@ -104,7 +103,7 @@ class Werewolf: """ 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: await ctx.send("No game running, cannot start") @@ -116,7 +115,7 @@ class Werewolf: """ Stops the current game """ - game = await self._get_game(ctx.guild) + game = await self._get_game(ctx) if not game: await ctx.send("No game running, cannot stop") @@ -130,7 +129,7 @@ class Werewolf: """ try: target_id = int(target_id) - except: + except ValueError: target_id = None if target_id is None: @@ -148,7 +147,7 @@ class Werewolf: # return # else: - game = await self._get_game(ctx.guild) + game = await self._get_game(ctx) if game is None: await ctx.send("No game running, cannot vote") @@ -186,17 +185,17 @@ class Werewolf: await game.choose(ctx, data) - async def _get_game(self, ctx, game_code=None): + async def _get_game(self, ctx: RedContext, game_code=None): if ctx.guild is None: # Private message, can't get guild + ctx.send("Cannot start game from PM!") return None if ctx.guild.id not in self.games or self.games[ctx.guild.id].game_over: await ctx.send("Starting a new game...") - if not game_code: - return None - role = await self.config.guild(ctx.guild).role() - role = discord.utils.get(ctx.guild.roles, id=role) + role_id = await self.config.guild(ctx.guild).role_id() + role = discord.utils.get(ctx.guild.roles, id=role_id) if role is None: + ctx.send("Game role is invalid, cannot start new game") return None self.games[ctx.guild.id] = Game(ctx.guild, role, game_code) From d73c45201501d29fdf1f94eb12a4aa60d6c8211e Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 24 Apr 2018 21:24:20 -0400 Subject: [PATCH 04/14] builder mostly done --- werewolf/builder.py | 298 +++++++++++++++++++++++++++++++++++----- werewolf/game.py | 38 ++--- werewolf/utils/menus.py | 134 ++++++++++++++++++ werewolf/werewolf.py | 14 +- 4 files changed, 434 insertions(+), 50 deletions(-) create mode 100644 werewolf/utils/menus.py diff --git a/werewolf/builder.py b/werewolf/builder.py index 0be61d9..0d2eee5 100644 --- a/werewolf/builder.py +++ b/werewolf/builder.py @@ -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) diff --git a/werewolf/game.py b/werewolf/game.py index be7d0e1..ec3d0ff 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -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 diff --git a/werewolf/utils/menus.py b/werewolf/utils/menus.py new file mode 100644 index 0000000..35b4fbd --- /dev/null +++ b/werewolf/utils/menus.py @@ -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, +} diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 4e31746..31bb88d 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -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): """ From 707b0453e9d69306c2bf011fce041916e736074d Mon Sep 17 00:00:00 2001 From: Bobloy Date: Wed, 25 Apr 2018 13:42:40 -0400 Subject: [PATCH 05/14] work it right --- werewolf/builder.py | 1 - werewolf/werewolf.py | 16 +++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/werewolf/builder.py b/werewolf/builder.py index 0d2eee5..14d187d 100644 --- a/werewolf/builder.py +++ b/werewolf/builder.py @@ -117,7 +117,6 @@ async def parse_code(code, game): if len(built) < digits: built += c - continue try: idx = int(built) diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 31bb88d..53ca7d0 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -123,11 +123,17 @@ class Werewolf: """ Stops the current game """ - game = await self._get_game(ctx) - if not game: - await ctx.send("No game running, cannot stop") + if ctx.guild is None: + # Private message, can't get guild + 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 + await ctx.sent("Game has been stopped") @commands.guild_only() @ww.command() @@ -196,14 +202,14 @@ class Werewolf: async def _get_game(self, ctx: RedContext, game_code=None): if ctx.guild is None: # Private message, can't get guild - ctx.send("Cannot start game from PM!") + await ctx.send("Cannot start game from PM!") return None if ctx.guild.id not in self.games or self.games[ctx.guild.id].game_over: await ctx.send("Starting a new game...") role_id = await self.config.guild(ctx.guild).role_id() role = discord.utils.get(ctx.guild.roles, id=role_id) if role is None: - ctx.send("Game role is invalid, cannot start new game") + await ctx.send("Game role is invalid, cannot start new game") return None self.games[ctx.guild.id] = Game(ctx.guild, role, game_code) From a7796a084b68111f844ea7acdea3aa4dd6cb5b3c Mon Sep 17 00:00:00 2001 From: Bobloy Date: Wed, 25 Apr 2018 13:47:04 -0400 Subject: [PATCH 06/14] context and typo --- werewolf/werewolf.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 53ca7d0..62157de 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -32,7 +32,7 @@ class Werewolf: del game @commands.command() - async def buildgame(self, ctx): + async def buildgame(self, ctx: RedContext): gb = GameBuilder() code = await gb.build_game(ctx) @@ -51,7 +51,7 @@ class Werewolf: @commands.guild_only() @wwset.command(name="role") - async def wwset_role(self, ctx, role: discord.Role): + async def wwset_role(self, ctx: RedContext, role: discord.Role): """ Assign the game role This role should not be manually assigned @@ -69,7 +69,7 @@ class Werewolf: @commands.guild_only() @ww.command() - async def new(self, ctx, game_code=None): + async def new(self, ctx: RedContext, game_code=None): """ Create and join a new game of Werewolf """ @@ -81,7 +81,7 @@ class Werewolf: @commands.guild_only() @ww.command() - async def join(self, ctx): + async def join(self, ctx: RedContext): """ Joins a game of Werewolf """ @@ -96,7 +96,7 @@ class Werewolf: @commands.guild_only() @ww.command() - async def quit(self, ctx): + async def quit(self, ctx: RedContext): """ Quit a game of Werewolf """ @@ -107,7 +107,7 @@ class Werewolf: @commands.guild_only() @ww.command() - async def start(self, ctx): + async def start(self, ctx: RedContext): """ Checks number of players and attempts to start the game """ @@ -119,7 +119,7 @@ class Werewolf: @commands.guild_only() @ww.command() - async def stop(self, ctx): + async def stop(self, ctx: RedContext): """ Stops the current game """ @@ -133,11 +133,11 @@ class Werewolf: game = await self._get_game(ctx) game.game_over = True - await ctx.sent("Game has been stopped") + await ctx.send("Game has been stopped") @commands.guild_only() @ww.command() - async def vote(self, ctx, target_id: int): + async def vote(self, ctx: RedContext, target_id: int): """ Vote for a player by ID """ @@ -177,7 +177,7 @@ class Werewolf: await ctx.send("Nothing to vote for in this channel") @ww.command() - async def choose(self, ctx, data): + async def choose(self, ctx: RedContext, data): """ Arbitrary decision making Handled by game+role From e46f799d02366797626c484237ab5c4ac59e3f6b Mon Sep 17 00:00:00 2001 From: Bobloy Date: Wed, 25 Apr 2018 14:09:03 -0400 Subject: [PATCH 07/14] fix code problems --- werewolf/builder.py | 2 +- werewolf/game.py | 15 +++++++++------ werewolf/werewolf.py | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/werewolf/builder.py b/werewolf/builder.py index 14d187d..0c3f5d1 100644 --- a/werewolf/builder.py +++ b/werewolf/builder.py @@ -136,7 +136,7 @@ async def parse_code(code, game): pass if not options: - raise ValueError("No Match Found") + raise IndexError("No Match Found") decode.append(choice(options)(game)) diff --git a/werewolf/game.py b/werewolf/game.py index ec3d0ff..8094b6a 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -1,6 +1,5 @@ import asyncio import random -from typing import List import discord from redbot.core import RedContext @@ -615,6 +614,11 @@ class Game: async def get_day_target(self, target_id, source=None): return self.players[target_id] # ToDo check source + async def set_code(self, ctx: RedContext, 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: self.game_code = game_code @@ -624,12 +628,11 @@ class Game: 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") + 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: return False diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 62157de..2c230af 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -94,6 +94,21 @@ class Werewolf: await game.join(ctx.author, ctx.channel) + @commands.guild_only() + @ww.command() + async def code(self, ctx: RedContext, 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() async def quit(self, ctx: RedContext): From cca219d20f5c154f9d09d8cda85d9825de5c3da9 Mon Sep 17 00:00:00 2001 From: Bobloy Date: Wed, 25 Apr 2018 17:03:04 -0400 Subject: [PATCH 08/14] progress --- werewolf/game.py | 57 +++++++++++++++++++++++++-------------- werewolf/werewolf.py | 63 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 95 insertions(+), 25 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index 8094b6a..217bf5c 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -26,7 +26,9 @@ class Game: day_vote_count = 3 - def __init__(self, guild: discord.Guild, role: discord.Role, game_code=None): + def __init__(self, guild: discord.Guild, role: discord.Role=None, + category: discord.CategoryChannel=None, village: discord.TextChannel=None, + game_code=None): self.guild = guild self.game_code = game_code self.game_role = role @@ -46,8 +48,8 @@ class Game: self.day_count = 0 self.ongoing_vote = False - self.channel_category = None # discord.CategoryChannel - self.village_channel = None # discord.TextChannel + self.channel_category = category # discord.CategoryChannel + self.village_channel = village # discord.TextChannel self.p_channels = {} # uses default_secret_channel self.vote_groups = {} # ID : VoteGroup() @@ -93,11 +95,16 @@ class Game: return False if self.game_role is None: - await ctx.send("Game role not configured, cannot start") - self.roles = [] - return False + try: + self.game_role = await ctx.guild.create_role(name="Players", + hoist=True, + mentionable=True, + reason="(BOT) Werewolf 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 - self.started = True await self.assign_roles() # Create category and channel with individual overwrites @@ -107,19 +114,29 @@ class Game: self.guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True, add_reactions=True), self.game_role: discord.PermissionOverwrite(read_messages=True, send_messages=True) } - - self.channel_category = await self.guild.create_category("ww-game", - overwrites=overwrite, - reason="(BOT) New game of werewolf") - - # for player in self.players: - # overwrite[player.member] = discord.PermissionOverwrite(read_messages=True) - - self.village_channel = await self.guild.create_text_channel("Village Square", - overwrites=overwrite, - reason="(BOT) New game of werewolf", - category=self.channel_category) - + if self.channel_category is None: + self.channel_category = await self.guild.create_category("ww-game", + overwrites=overwrite, + reason="(BOT) New game of werewolf") + else: + 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: + self.village_channel = await self.guild.create_text_channel("Village Square", + overwrites=overwrite, + reason="(BOT) New game of werewolf", + category=self.channel_category) + else: + await self.village_channel.edit(name="Village Square", + category=self.channel_category, + reason="(BOT) New game of werewolf") + for target, ow in overwrite.items(): + await self.village_channel.set_permissions(target=target, + overwrite=ow, + reason="(BOT) New game of werewolf") + self.started = True # Assuming everything worked so far print("Pre at_game_start") await self._at_game_start() # This will queue channels and votegroups to be made diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 2c230af..89420d9 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -18,7 +18,10 @@ class Werewolf: self.config = Config.get_conf(self, identifier=87101114101119111108102, force_registration=True) default_global = {} default_guild = { - "role_id": None + "role_id": None, + "category_id": None, + "channel_id": None, + "log_channel_id": None } self.config.register_global(**default_global) @@ -59,6 +62,38 @@ class Werewolf: 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: RedContext, category_id): + """ + Assign the channel category + """ + + 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("Channel Category has been set to **{}**".format(category.name)) + + @commands.guild_only() + @wwset.command(name="channel") + async def wwset_channel(self, ctx: RedContext, channel: discord.TextChannel): + """ + Assign the village channel + """ + 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_channel(self, ctx: RedContext, channel: discord.TextChannel): + """ + Assign the log channel + """ + await self.config.guild(ctx.guild).log_channel_id.set(channel.id) + await ctx.send("Log Channel has been set to **{}**".format(channel.mention)) + @commands.group() async def ww(self, ctx: RedContext): """ @@ -221,11 +256,29 @@ class Werewolf: return None if ctx.guild.id not in self.games or self.games[ctx.guild.id].game_over: await ctx.send("Starting a new game...") + role = None + category = None + channel = None + log_channel = None + role_id = await self.config.guild(ctx.guild).role_id() - role = discord.utils.get(ctx.guild.roles, id=role_id) - if role is None: - await ctx.send("Game role is invalid, cannot start new game") - return None + category_id = await self.config.guild(ctx.guild).category_id() + channel_id = await self.config.guild(ctx.guild).channel_id() + log_channel_id = await self.config.guild(ctx.guild).log_channel_id() + + if role_id is not None: + role = discord.utils.get(ctx.guild.roles, id=role_id) + if role is None: + await ctx.send("Game role is invalid, cannot start new game") + return None + if category_id is not None: + category = discord.utils.get(ctx.guild.categories, id=category_id) + if role is None: + await ctx.send("Game role is invalid, cannot start new game") + return None + + + self.games[ctx.guild.id] = Game(ctx.guild, role, game_code) return self.games[ctx.guild.id] From 5832f147b0cda8d097c92bf7e60c7838209bb92e Mon Sep 17 00:00:00 2001 From: Bobloy Date: Fri, 27 Apr 2018 14:58:39 -0400 Subject: [PATCH 09/14] role searches --- werewolf/builder.py | 24 +++++++++++++ werewolf/game.py | 4 +-- werewolf/role.py | 6 ++++ werewolf/werewolf.py | 85 ++++++++++++++++++++++++++++++++++---------- 4 files changed, 98 insertions(+), 21 deletions(-) diff --git a/werewolf/builder.py b/werewolf/builder.py index 0c3f5d1..03a84c7 100644 --- a/werewolf/builder.py +++ b/werewolf/builder.py @@ -218,6 +218,30 @@ async def prev_group(ctx: RedContext, pages: list, 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): roles = [ROLE_LIST[idx] for idx in code_list] embed = discord.Embed(title="Currently selected roles") diff --git a/werewolf/game.py b/werewolf/game.py index 217bf5c..acc045a 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -26,8 +26,8 @@ class Game: day_vote_count = 3 - def __init__(self, guild: discord.Guild, role: discord.Role=None, - category: discord.CategoryChannel=None, village: discord.TextChannel=None, + def __init__(self, guild: discord.Guild, role: discord.Role = None, + category: discord.CategoryChannel = None, village: discord.TextChannel = None, game_code=None): self.guild = guild self.game_code = game_code diff --git a/werewolf/role.py b/werewolf/role.py index afede03..a2e0a52 100644 --- a/werewolf/role.py +++ b/werewolf/role.py @@ -46,6 +46,12 @@ class Role: "You win by testing the game\n" "Lynch players during the day with `[p]ww vote `" ) + 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): self.game = game diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 89420d9..65760c0 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -4,8 +4,9 @@ from redbot.core import Config from redbot.core import RedContext from redbot.core.bot import Red -from werewolf.builder import GameBuilder +from werewolf.builder import GameBuilder, role_from_name, role_from_alignment, role_from_category, role_from_id from werewolf.game import Game +from werewolf.utils.menus import menu, DEFAULT_CONTROLS class Werewolf: @@ -103,8 +104,8 @@ class Werewolf: await ctx.send_help() @commands.guild_only() - @ww.command() - async def new(self, ctx: RedContext, game_code=None): + @ww.command(name="new") + async def ww_new(self, ctx: RedContext, game_code=None): """ Create and join a new game of Werewolf """ @@ -115,8 +116,8 @@ class Werewolf: await ctx.send("Game is ready to join! Use `[p]ww join`") @commands.guild_only() - @ww.command() - async def join(self, ctx: RedContext): + @ww.command(name="join") + async def ww_join(self, ctx: RedContext): """ Joins a game of Werewolf """ @@ -130,8 +131,8 @@ class Werewolf: await game.join(ctx.author, ctx.channel) @commands.guild_only() - @ww.command() - async def code(self, ctx: RedContext, code): + @ww.command(name="code") + async def ww_code(self, ctx: RedContext, code): """ Adjust game code """ @@ -145,8 +146,8 @@ class Werewolf: await game.set_code(ctx, code) @commands.guild_only() - @ww.command() - async def quit(self, ctx: RedContext): + @ww.command(name="quit") + async def ww_quit(self, ctx: RedContext): """ Quit a game of Werewolf """ @@ -156,8 +157,8 @@ class Werewolf: await game.quit(ctx.author, ctx.channel) @commands.guild_only() - @ww.command() - async def start(self, ctx: RedContext): + @ww.command(name="start") + async def ww_start(self, ctx: RedContext): """ Checks number of players and attempts to start the game """ @@ -168,8 +169,8 @@ class Werewolf: await game.setup(ctx) @commands.guild_only() - @ww.command() - async def stop(self, ctx: RedContext): + @ww.command(name="stop") + async def ww_stop(self, ctx: RedContext): """ Stops the current game """ @@ -186,8 +187,8 @@ class Werewolf: await ctx.send("Game has been stopped") @commands.guild_only() - @ww.command() - async def vote(self, ctx: RedContext, target_id: int): + @ww.command(name="vote") + async def ww_vote(self, ctx: RedContext, target_id: int): """ Vote for a player by ID """ @@ -226,8 +227,8 @@ class Werewolf: else: await ctx.send("Nothing to vote for in this channel") - @ww.command() - async def choose(self, ctx: RedContext, data): + @ww.command(name="choose") + async def ww_choose(self, ctx: RedContext, data): """ Arbitrary decision making Handled by game+role @@ -249,6 +250,54 @@ class Werewolf: await game.choose(ctx, data) + @ww.group(name="search") + async def ww_search(self, ctx: RedContext): + """ + 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: RedContext, *, 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: RedContext, 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: RedContext, 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: RedContext, 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: RedContext, game_code=None): if ctx.guild is None: # Private message, can't get guild @@ -277,8 +326,6 @@ class Werewolf: await ctx.send("Game role is invalid, cannot start new game") return None - - self.games[ctx.guild.id] = Game(ctx.guild, role, game_code) return self.games[ctx.guild.id] From 8dabc12c97e78766dd89a41a0872634417b0a82f Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 30 Apr 2018 09:53:55 -0400 Subject: [PATCH 10/14] settings --- werewolf/game.py | 39 +++++++++++++++++++++------------------ werewolf/werewolf.py | 44 ++++++++++++++++++++++++++------------------ 2 files changed, 47 insertions(+), 36 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index 217bf5c..4ab529f 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -28,7 +28,7 @@ class Game: def __init__(self, guild: discord.Guild, role: discord.Role=None, category: discord.CategoryChannel=None, village: discord.TextChannel=None, - game_code=None): + log_channel: discord.TextChannel=None, game_code=None): self.guild = guild self.game_code = game_code self.game_role = role @@ -50,6 +50,7 @@ class Game: self.channel_category = category # discord.CategoryChannel self.village_channel = village # discord.TextChannel + self.log_channel = log_channel self.p_channels = {} # uses default_secret_channel self.vote_groups = {} # ID : VoteGroup() @@ -58,20 +59,20 @@ class Game: self.loop = asyncio.get_event_loop() - def __del__(self): - """ - Cleanup channels as necessary - :return: - """ - - print("Delete is called") - - self.game_over = True - if self.village_channel: - asyncio.ensure_future(self.village_channel.delete("Werewolf game-over")) - - for c_data in self.p_channels.values(): - asyncio.ensure_future(c_data["channel"].delete("Werewolf game-over")) + # def __del__(self): + # """ + # Cleanup channels as necessary + # :return: + # """ + # + # print("Delete is called") + # + # self.game_over = True + # if self.village_channel: + # asyncio.ensure_future(self.village_channel.delete("Werewolf game-over")) + # + # for c_data in self.p_channels.values(): + # asyncio.ensure_future(c_data["channel"].delete("Werewolf game-over")) async def setup(self, ctx: RedContext): """ @@ -115,16 +116,17 @@ class Game: 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("ww-game", + self.channel_category = await self.guild.create_category("πŸ”΄ Werewolf Game (ACTIVE)", overwrites=overwrite, reason="(BOT) New game of werewolf") else: + 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: - self.village_channel = await self.guild.create_text_channel("Village Square", + self.village_channel = await self.guild.create_text_channel("village-square", overwrites=overwrite, reason="(BOT) New game of werewolf", category=self.channel_category) @@ -744,4 +746,5 @@ class Game: reason = '(BOT) End of WW game' await self.village_channel.set_permissions(self.game_role, overwrite=None, reason=reason) await self.channel_category.set_permissions(self.game_role, overwrite=None, reason=reason) - await self.channel_category.edit(reason=reason, name="Archived Game") + await self.channel_category.edit(reason=reason, name="Werewolf Game (INACTIVE)") + # Optional dynamic channels/categories diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 89420d9..ebd15e9 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -7,7 +7,6 @@ from redbot.core.bot import Red from werewolf.builder import GameBuilder from werewolf.game import Game - class Werewolf: """ Base to host werewolf on a guild @@ -87,7 +86,7 @@ class Werewolf: @commands.guild_only() @wwset.command(name="logchannel") - async def wwset_channel(self, ctx: RedContext, channel: discord.TextChannel): + async def wwset_log_channel(self, ctx: RedContext, channel: discord.TextChannel): """ Assign the log channel """ @@ -237,7 +236,6 @@ class Werewolf: if ctx.guild is not None: await ctx.send("This action is only available in DM's") return - # DM nonsense, find their game # If multiple games, panic for game in self.games.values(): @@ -250,38 +248,48 @@ class Werewolf: await game.choose(ctx, data) async def _get_game(self, ctx: RedContext, game_code=None): - if ctx.guild is None: + guild: discord.Guild = ctx.guild + + if guild is None: # Private message, can't get guild await ctx.send("Cannot start game from PM!") return None - if ctx.guild.id not in self.games or self.games[ctx.guild.id].game_over: + if guild.id not in self.games or self.games[guild.id].game_over: await ctx.send("Starting a new game...") role = None category = None channel = None log_channel = None - role_id = await self.config.guild(ctx.guild).role_id() - category_id = await self.config.guild(ctx.guild).category_id() - channel_id = await self.config.guild(ctx.guild).channel_id() - log_channel_id = await self.config.guild(ctx.guild).log_channel_id() + 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(ctx.guild.roles, id=role_id) + role = discord.utils.get(guild.roles, id=role_id) if role is None: - await ctx.send("Game role is invalid, cannot start new game") + await ctx.send("Game Role is invalid, cannot start new game") return None if category_id is not None: - category = discord.utils.get(ctx.guild.categories, id=category_id) - if role is None: - await ctx.send("Game role is invalid, cannot start new game") + category = discord.utils.get(guild.categories, id=category_id) + if category is None: + await ctx.send("Game Category is invalid, cannot start new game") + return 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, cannot start new game") + return 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, cannot start new game") return None + self.games[guild.id] = Game(guild, role, category, channel, log_channel, game_code) - - self.games[ctx.guild.id] = Game(ctx.guild, role, game_code) - - return self.games[ctx.guild.id] + return self.games[guild.id] async def _game_start(self, game): await game.start() From d206ea2cb91082a70fe52132834d023b0428f22f Mon Sep 17 00:00:00 2001 From: Bobloy Date: Tue, 1 May 2018 17:05:08 -0400 Subject: [PATCH 11/14] Errors and permissions --- werewolf/game.py | 105 ++++++++++++++++++++++--------- werewolf/werewolf.py | 144 ++++++++++++++++++++++++++++--------------- 2 files changed, 171 insertions(+), 78 deletions(-) diff --git a/werewolf/game.py b/werewolf/game.py index 4ab529f..a46d2ce 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -26,12 +26,11 @@ class Game: day_vote_count = 3 - def __init__(self, guild: discord.Guild, role: discord.Role=None, - category: discord.CategoryChannel=None, village: discord.TextChannel=None, - log_channel: discord.TextChannel=None, game_code=None): + def __init__(self, guild: discord.Guild, role: discord.Role = None, + category: discord.CategoryChannel = None, village: discord.TextChannel = None, + log_channel: discord.TextChannel = None, game_code=None): self.guild = guild self.game_code = game_code - self.game_role = role self.roles = [] # List[Role] self.players = [] # List[Player] @@ -48,10 +47,14 @@ class Game: 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.to_delete = set() + self.save_perms = {} + self.p_channels = {} # uses default_secret_channel self.vote_groups = {} # ID : VoteGroup() @@ -97,14 +100,22 @@ class Game: if self.game_role is None: try: - self.game_role = await ctx.guild.create_role(name="Players", + self.game_role = await ctx.guild.create_role(name="WW Players", 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() @@ -112,32 +123,54 @@ class Game: overwrite = { self.guild.default_role: discord.PermissionOverwrite(read_messages=True, send_messages=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) } if self.channel_category is None: - self.channel_category = await self.guild.create_category("πŸ”΄ Werewolf Game (ACTIVE)", + self.channel_category = await self.guild.create_category("Werewolf Game", overwrites=overwrite, reason="(BOT) New game of werewolf") - else: - 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") + 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: - self.village_channel = await self.guild.create_text_channel("village-square", - overwrites=overwrite, - reason="(BOT) New game of werewolf", - category=self.channel_category) + 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: - await self.village_channel.edit(name="Village Square", - category=self.channel_category, - reason="(BOT) New game of werewolf") - for target, ow in overwrite.items(): - await self.village_channel.set_permissions(target=target, - overwrite=ow, - reason="(BOT) New game of werewolf") + 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") + + try: + for target, ow in overwrite.items(): + curr = self.village_channel.overwrites_for(target) + curr.update(**{perm: value for perm, value in ow}) + await self.village_channel.set_permissions(target=target, + overwrite=curr, + reason="(BOT) New game of werewolf") + except discord.Forbidden: + await ctx.send("Unable to edit Game Channel permissions\n" + "Grant Bot appropriate permissions to manage permissions") + return self.started = True # Assuming everything worked so far print("Pre at_game_start") @@ -147,7 +180,9 @@ class Game: print("Channel id: " + channel_id) overwrite = { 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"]: @@ -433,7 +468,12 @@ class Game: self.players.append(Player(member)) - await member.add_roles(*[self.game_role]) + 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, " "total players is **{}**".format(member.mention, len(self.players))) @@ -744,7 +784,14 @@ class Game: async def _end_game(self): # Remove game_role access for potential archiving for now reason = '(BOT) End of WW game' - await self.village_channel.set_permissions(self.game_role, overwrite=None, reason=reason) - await self.channel_category.set_permissions(self.game_role, overwrite=None, reason=reason) - await self.channel_category.edit(reason=reason, name="Werewolf Game (INACTIVE)") + for obj in self.to_delete: + print(obj) + await obj.delete(reason=reason) + + try: + await self.village_channel.edit(reason=reason, name="Werewolf") + 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 diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 60ce051..1f00dbc 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -1,6 +1,6 @@ import discord 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 @@ -8,6 +8,7 @@ from werewolf.builder import GameBuilder, role_from_name, role_from_alignment, r from werewolf.game import Game from werewolf.utils.menus import menu, DEFAULT_CONTROLS + class Werewolf: """ Base to host werewolf on a guild @@ -44,6 +45,7 @@ class Werewolf: else: await ctx.send("No code generated") + @checks.guildowner() @commands.group() async def wwset(self, ctx: RedContext): """ @@ -52,47 +54,80 @@ class Werewolf: if ctx.invoked_subcommand is None: await ctx.send_help() + @commands.guild_only() + @wwset.command(name="list") + async def wwset_list(self, ctx: RedContext): + """ + 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() @wwset.command(name="role") - async def wwset_role(self, ctx: RedContext, role: discord.Role): + async def wwset_role(self, ctx: RedContext, role: discord.Role=None): """ Assign the game role This role should not be manually assigned """ - await self.config.guild(ctx.guild).role_id.set(role.id) - await ctx.send("Game role has been set to **{}**".format(role.name)) + if role is None: + 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: RedContext, category_id): + async def wwset_category(self, ctx: RedContext, category_id=None): """ Assign the channel category """ - - 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("Channel Category has been set to **{}**".format(category.name)) + 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: RedContext, channel: discord.TextChannel): + async def wwset_channel(self, ctx: RedContext, channel: discord.TextChannel=None): """ Assign the village channel """ - await self.config.guild(ctx.guild).channel_id.set(channel.id) - await ctx.send("Game Channel has been set to **{}**".format(channel.mention)) + 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: RedContext, channel: discord.TextChannel): + async def wwset_log_channel(self, ctx: RedContext, channel: discord.TextChannel=None): """ Assign the log channel """ - await self.config.guild(ctx.guild).log_channel_id.set(channel.id) - await ctx.send("Log Channel has been set to **{}**".format(channel.mention)) + 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() async def ww(self, ctx: RedContext): @@ -165,7 +200,8 @@ class Werewolf: if not game: await ctx.send("No game running, cannot start") - await game.setup(ctx) + if not await game.setup(ctx): + pass # Do something? @commands.guild_only() @ww.command(name="stop") @@ -305,36 +341,11 @@ class Werewolf: return None if guild.id not in self.games or self.games[guild.id].game_over: await ctx.send("Starting a new game...") - 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, cannot start new game") - return 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, cannot start new game") - return 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, cannot start new game") - return 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, cannot start new game") - return None + success, role, category, channel, log_channel = await self._get_settings(ctx) + + if not success: + await ctx.send("Cannot start a new game") + return None self.games[guild.id] = Game(guild, role, category, channel, log_channel, game_code) @@ -342,3 +353,38 @@ class Werewolf: async def _game_start(self, game): 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 \ No newline at end of file From 4d9ac39e82c50c33916d13e6a744502b48769c77 Mon Sep 17 00:00:00 2001 From: Bobloy Date: Wed, 2 May 2018 14:08:06 -0400 Subject: [PATCH 12/14] string roles and other nonsense --- werewolf/builder.py | 10 +++++----- werewolf/game.py | 4 +++- werewolf/werewolf.py | 3 ++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/werewolf/builder.py b/werewolf/builder.py index 03a84c7..572fb9d 100644 --- a/werewolf/builder.py +++ b/werewolf/builder.py @@ -164,21 +164,21 @@ async def encode(roles, rand_roles): if digit_sort: out_code += "T" for role in digit_sort: - out_code += role + 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 += role + 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 += role + out_code += str(role) return out_code @@ -242,7 +242,7 @@ def role_from_name(name: str): for idx, role in enumerate(ROLE_LIST) if name in role.__name__] -def say_role_list(code_list): +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) @@ -290,7 +290,7 @@ class GameBuilder: except discord.NotFound: pass - await ctx.send(embed=say_role_list(self.code)) + await ctx.send(embed=say_role_list(self.code, self.rand_roles)) return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout) diff --git a/werewolf/game.py b/werewolf/game.py index a46d2ce..22d9289 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -150,7 +150,7 @@ class Game: "Grant Bot appropriate permissions or assign a game_channel") return False else: - self.save_perms[self.village_channel] = self.village_channel.overwrites() + self.save_perms[self.village_channel] = self.village_channel.overwrites try: await self.village_channel.edit(name="πŸ”΅Werewolf", category=self.channel_category, @@ -790,6 +790,8 @@ class Game: 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 diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 1f00dbc..e513678 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -387,4 +387,5 @@ class Werewolf: await ctx.send("Log Channel is invalid") return False, None, None, None, None - return True, role, category, channel, log_channel \ No newline at end of file + return True, role, category, channel, log_channel + From 03bd13309e4e5a1b49fa0b5f4b6901e048b13589 Mon Sep 17 00:00:00 2001 From: Bobloy Date: Wed, 2 May 2018 15:17:19 -0400 Subject: [PATCH 13/14] better pages --- werewolf/builder.py | 22 ++++++++++++++-------- werewolf/roles/seer.py | 2 +- werewolf/roles/villager.py | 2 +- werewolf/werewolf.py | 2 +- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/werewolf/builder.py b/werewolf/builder.py index 572fb9d..5113a0b 100644 --- a/werewolf/builder.py +++ b/werewolf/builder.py @@ -21,7 +21,7 @@ WW_ROLES = [(idx, role) for idx, role in enumerate(ROLE_LIST) if role.alignment OTHER_ROLES = [(idx, role) for idx, role in enumerate(ROLE_LIST) if role.alignment not in [0, 1]] ROLE_PAGES = [] -PAGE_GROUPS = [] +PAGE_GROUPS = [0] ROLE_CATEGORIES = { 1: "Random", 2: "Investigative", 3: "Protective", 4: "Government", @@ -45,9 +45,6 @@ def role_embed(idx, role, color): 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: @@ -61,7 +58,7 @@ def setup(): 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)) + ROLE_PAGES.append(discord.Embed(title="RANDOM:Town Role", description="Town {}".format(v), color=0x008000)) CATEGORY_COUNT.append(k) # Random WW Roles @@ -70,7 +67,7 @@ def setup(): 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)) + 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: @@ -78,7 +75,7 @@ def setup(): 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)) + discord.Embed(title="RANDOM:Neutral Role", description="Neutral {}".format(v), color=0xc0c0c0)) CATEGORY_COUNT.append(k) @@ -117,6 +114,7 @@ async def parse_code(code, game): if len(built) < digits: built += c + continue try: idx = int(built) @@ -249,6 +247,14 @@ def say_role_list(code_list, rand_roles): 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) @@ -306,7 +312,7 @@ class GameBuilder: pass if page >= len(ROLE_LIST): - self.rand_roles.append(CATEGORY_COUNT[len(ROLE_LIST) - page]) + self.rand_roles.append(CATEGORY_COUNT[page-len(ROLE_LIST)]) else: self.code.append(page) diff --git a/werewolf/roles/seer.py b/werewolf/roles/seer.py index 7b3fe5d..96260d9 100644 --- a/werewolf/roles/seer.py +++ b/werewolf/roles/seer.py @@ -2,7 +2,7 @@ from werewolf.role import 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) alignment = 1 # 1: Town, 2: Werewolf, 3: Neutral channel_id = "" # Empty for no private channel diff --git a/werewolf/roles/villager.py b/werewolf/roles/villager.py index f1b8016..4935275 100644 --- a/werewolf/roles/villager.py +++ b/werewolf/roles/villager.py @@ -2,7 +2,7 @@ from werewolf.role import 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) alignment = 1 # 1: Town, 2: Werewolf, 3: Neutral channel_id = "" # Empty for no private channel diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index e513678..97bbede 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -40,7 +40,7 @@ class Werewolf: gb = GameBuilder() code = await gb.build_game(ctx) - if code is not None: + if code != "": await ctx.send("Your game code is **{}**".format(code)) else: await ctx.send("No code generated") From 2cd594c36a424c928ea993b7d0834043a8024178 Mon Sep 17 00:00:00 2001 From: Bobloy Date: Fri, 4 May 2018 08:48:36 -0400 Subject: [PATCH 14/14] commands.Context for the future --- werewolf/builder.py | 14 ++--- werewolf/game.py | 6 +- werewolf/utils/menus.py | 134 ---------------------------------------- werewolf/werewolf.py | 48 +++++++------- 4 files changed, 34 insertions(+), 168 deletions(-) delete mode 100644 werewolf/utils/menus.py diff --git a/werewolf/builder.py b/werewolf/builder.py index 5113a0b..4a9da6a 100644 --- a/werewolf/builder.py +++ b/werewolf/builder.py @@ -3,13 +3,13 @@ from collections import defaultdict from random import choice import discord -from redbot.core import RedContext +from discord.ext import commands # Import all roles here from werewolf.roles.seer import Seer from werewolf.roles.vanillawerewolf import VanillaWerewolf from werewolf.roles.villager import Villager -from werewolf.utils.menus import menu, prev_page, next_page, close_menu +from redbot.core.utils.menus import menu, prev_page, next_page, close_menu # All roles in this list for iterating @@ -181,7 +181,7 @@ async def encode(roles, rand_roles): return out_code -async def next_group(ctx: RedContext, pages: list, +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) @@ -201,7 +201,7 @@ async def next_group(ctx: RedContext, pages: list, page=page, timeout=timeout) -async def prev_group(ctx: RedContext, pages: list, +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) @@ -268,7 +268,7 @@ class GameBuilder: self.rand_roles = [] setup() - async def build_game(self, ctx: RedContext): + async def build_game(self, ctx: commands.Context): new_controls = { 'βͺ': prev_group, "β¬…": prev_page, @@ -286,7 +286,7 @@ class GameBuilder: out = await encode(self.code, self.rand_roles) return out - async def list_roles(self, ctx: RedContext, pages: list, + 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) @@ -301,7 +301,7 @@ class GameBuilder: return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout) - async def select_page(self, ctx: RedContext, pages: list, + 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) diff --git a/werewolf/game.py b/werewolf/game.py index 22d9289..1a6641f 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -2,7 +2,7 @@ import asyncio import random import discord -from redbot.core import RedContext +from discord.ext import commands from werewolf.builder import parse_code from werewolf.player import Player @@ -77,7 +77,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: RedContext): + async def setup(self, ctx: commands.Context): """ Runs the initial setup @@ -673,7 +673,7 @@ class Game: async def get_day_target(self, target_id, source=None): return self.players[target_id] # ToDo check source - async def set_code(self, ctx: RedContext, game_code): + 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") diff --git a/werewolf/utils/menus.py b/werewolf/utils/menus.py deleted file mode 100644 index 35b4fbd..0000000 --- a/werewolf/utils/menus.py +++ /dev/null @@ -1,134 +0,0 @@ -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, -} diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 97bbede..65e73c1 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -1,12 +1,12 @@ import discord from discord.ext import commands 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.utils.menus import menu, DEFAULT_CONTROLS +from redbot.core.utils.menus import menu, DEFAULT_CONTROLS class Werewolf: @@ -36,7 +36,7 @@ class Werewolf: del game @commands.command() - async def buildgame(self, ctx: RedContext): + async def buildgame(self, ctx: commands.Context): gb = GameBuilder() code = await gb.build_game(ctx) @@ -47,7 +47,7 @@ class Werewolf: @checks.guildowner() @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. """ @@ -56,7 +56,7 @@ class Werewolf: @commands.guild_only() @wwset.command(name="list") - async def wwset_list(self, ctx: RedContext): + async def wwset_list(self, ctx: commands.Context): """ Lists current guild settings """ @@ -74,7 +74,7 @@ class Werewolf: @commands.guild_only() @wwset.command(name="role") - async def wwset_role(self, ctx: RedContext, role: discord.Role=None): + async def wwset_role(self, ctx: commands.Context, role: discord.Role=None): """ Assign the game role This role should not be manually assigned @@ -88,7 +88,7 @@ class Werewolf: @commands.guild_only() @wwset.command(name="category") - async def wwset_category(self, ctx: RedContext, category_id=None): + async def wwset_category(self, ctx: commands.Context, category_id=None): """ Assign the channel category """ @@ -105,7 +105,7 @@ class Werewolf: @commands.guild_only() @wwset.command(name="channel") - async def wwset_channel(self, ctx: RedContext, channel: discord.TextChannel=None): + async def wwset_channel(self, ctx: commands.Context, channel: discord.TextChannel=None): """ Assign the village channel """ @@ -118,7 +118,7 @@ class Werewolf: @commands.guild_only() @wwset.command(name="logchannel") - async def wwset_log_channel(self, ctx: RedContext, channel: discord.TextChannel=None): + async def wwset_log_channel(self, ctx: commands.Context, channel: discord.TextChannel=None): """ Assign the log channel """ @@ -130,7 +130,7 @@ class Werewolf: await ctx.send("Game Log Channel has been set to **{}**".format(channel.mention)) @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. """ @@ -139,7 +139,7 @@ class Werewolf: @commands.guild_only() @ww.command(name="new") - async def ww_new(self, ctx: RedContext, game_code=None): + async def ww_new(self, ctx: commands.Context, game_code=None): """ Create and join a new game of Werewolf """ @@ -151,7 +151,7 @@ class Werewolf: @commands.guild_only() @ww.command(name="join") - async def ww_join(self, ctx: RedContext): + async def ww_join(self, ctx: commands.Context): """ Joins a game of Werewolf """ @@ -166,7 +166,7 @@ class Werewolf: @commands.guild_only() @ww.command(name="code") - async def ww_code(self, ctx: RedContext, code): + async def ww_code(self, ctx: commands.Context, code): """ Adjust game code """ @@ -181,7 +181,7 @@ class Werewolf: @commands.guild_only() @ww.command(name="quit") - async def ww_quit(self, ctx: RedContext): + async def ww_quit(self, ctx: commands.Context): """ Quit a game of Werewolf """ @@ -192,7 +192,7 @@ class Werewolf: @commands.guild_only() @ww.command(name="start") - async def ww_start(self, ctx: RedContext): + async def ww_start(self, ctx: commands.Context): """ Checks number of players and attempts to start the game """ @@ -205,7 +205,7 @@ class Werewolf: @commands.guild_only() @ww.command(name="stop") - async def ww_stop(self, ctx: RedContext): + async def ww_stop(self, ctx: commands.Context): """ Stops the current game """ @@ -223,7 +223,7 @@ class Werewolf: @commands.guild_only() @ww.command(name="vote") - async def ww_vote(self, ctx: RedContext, target_id: int): + async def ww_vote(self, ctx: commands.Context, target_id: int): """ Vote for a player by ID """ @@ -263,7 +263,7 @@ class Werewolf: await ctx.send("Nothing to vote for in this channel") @ww.command(name="choose") - async def ww_choose(self, ctx: RedContext, data): + async def ww_choose(self, ctx: commands.Context, data): """ Arbitrary decision making Handled by game+role @@ -285,7 +285,7 @@ class Werewolf: await game.choose(ctx, data) @ww.group(name="search") - async def ww_search(self, ctx: RedContext): + async def ww_search(self, ctx: commands.Context): """ Find custom roles by name, alignment, category, or ID """ @@ -293,7 +293,7 @@ class Werewolf: await ctx.send_help() @ww_search.command(name="name") - async def ww_search_name(self, ctx: RedContext, *, 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) @@ -303,7 +303,7 @@ class Werewolf: await ctx.send("No roles containing that name were found") @ww_search.command(name="alignment") - async def ww_search_alignment(self, ctx: RedContext, alignment: int): + 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) @@ -313,7 +313,7 @@ class Werewolf: await ctx.send("No roles with that alignment were found") @ww_search.command(name="category") - async def ww_search_category(self, ctx: RedContext, category: int): + 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) @@ -323,7 +323,7 @@ class Werewolf: await ctx.send("No roles in that category were found") @ww_search.command(name="index") - async def ww_search_index(self, ctx: RedContext, idx: int): + 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) @@ -332,7 +332,7 @@ class Werewolf: else: await ctx.send("Role ID not found") - async def _get_game(self, ctx: RedContext, game_code=None): + async def _get_game(self, ctx: commands.Context, game_code=None): guild: discord.Guild = ctx.guild if guild is None: