From d0facafb9260e32c0d75b1ab0758a076bb93421f Mon Sep 17 00:00:00 2001 From: Bobloy Date: Mon, 23 Apr 2018 17:05:58 -0400 Subject: [PATCH] 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)