diff --git a/werewolf/game.py b/werewolf/game.py index c5aaad6..c0a9db4 100644 --- a/werewolf/game.py +++ b/werewolf/game.py @@ -10,13 +10,15 @@ from redbot.core.bot import Red from redbot.core.utils import AsyncIter from werewolf.builder import parse_code +from werewolf.constants import ALIGNMENT_NEUTRAL from werewolf.player import Player from werewolf.role import Role from werewolf.votegroup import VoteGroup log = logging.getLogger("red.fox_v3.werewolf.game") -HALF_DAY_LENGTH = 24 # FixMe: to 120 later for 4 minute days +HALF_DAY_LENGTH = 60 # FixMe: Make configurable +HALF_NIGHT_LENGTH = 60 async def anyone_has_role( @@ -167,7 +169,7 @@ class Game: await player.member.add_roles(*[self.game_role]) except discord.Forbidden: log.exception(f"Unable to add role **{self.game_role.name}**") - await ctx.send( + await ctx.maybe_send_embed( f"Unable to add role **{self.game_role.name}**\n" f"Bot is missing `manage_roles` permissions" ) @@ -210,7 +212,7 @@ class Game: category=self.channel_category, ) except discord.Forbidden: - await ctx.send( + await ctx.maybe_send_embed( "Unable to create Game Channel and none was provided\n" "Grant Bot appropriate permissions or assign a game_channel" ) @@ -225,7 +227,7 @@ class Game: ) except discord.Forbidden as e: log.exception("Unable to rename Game Channel") - await ctx.send("Unable to rename Game Channel, ignoring") + await ctx.maybe_send_embed("Unable to rename Game Channel, ignoring") try: for target, ow in overwrite.items(): @@ -235,7 +237,7 @@ class Game: target=target, overwrite=curr, reason="(BOT) New game of werewolf" ) except discord.Forbidden: - await ctx.send( + await ctx.maybe_send_embed( "Unable to edit Game Channel permissions\n" "Grant Bot appropriate permissions to manage permissions" ) @@ -406,14 +408,17 @@ class Game: await vote_message.add_reaction("👎") await asyncio.sleep(15) - reaction_list = vote_message.reactions - if True: # TODO: Allow customizable vote history deletion. - await vote_message.delete() + # Refetch for reactions + vote_message = await self.village_channel.fetch_message(id=vote_message.id) + reaction_list = vote_message.reactions raw_up_votes = sum(p for p in reaction_list if p.emoji == "👍" and not p.me) raw_down_votes = sum(p for p in reaction_list if p.emoji == "👎" and not p.me) + if True: # TODO: Allow customizable vote history deletion. + await vote_message.delete() + # TODO: Support vote count modifying roles. (Need notify and count function) voted_to_lynch = raw_down_votes > raw_up_votes @@ -492,13 +497,13 @@ class Game: return await self._notify("at_night_start") - await asyncio.sleep(12) # 2 minutes FixMe to 120 later + await asyncio.sleep(HALF_NIGHT_LENGTH) # 2 minutes FixMe to 120 later await self.village_channel.send( - embed=discord.Embed(title="**Two minutes of night remain...**") + embed=discord.Embed(title=f"**{HALF_NIGHT_LENGTH / 60} minutes of night remain...**") ) - await asyncio.sleep(9) # 1.5 minutes FixMe to 90 later + await asyncio.sleep(HALF_NIGHT_LENGTH) # 1.5 minutes FixMe to 90 later await self.village_channel.send( - embed=discord.Embed(title="**Thirty seconds until sunrise...**") + embed=discord.Embed(title=f"**{HALF_NIGHT_LENGTH / 60} minutes until sunrise...**") ) await asyncio.sleep(3) # .5 minutes FixMe to 30 Later @@ -560,8 +565,7 @@ class Game: ) else: embed.add_field( - name=f"{i} - {status}{player.member.display_name}", - inline=False, + name=f"{i} - {status}{player.member.display_name}", inline=False, value="" ) return await channel.send(embed=embed) @@ -585,16 +589,16 @@ class Game: if votegroup is not None: self.p_channels[channel_id]["votegroup"] = votegroup - async def join(self, member: discord.Member, channel: discord.TextChannel): + async def join(self, ctx, member: discord.Member): """ Have a member join a game """ if self.started: - await channel.send("**Game has already started!**") + await ctx.maybe_send_embed("**Game has already started!**") return if await self.get_player_by_member(member) is not None: - await channel.send(f"{member.display_name} is already in the game!") + await ctx.maybe_send_embed(f"{member.display_name} is already in the game!") return self.players.append(Player(member)) @@ -609,7 +613,7 @@ class Game: # f"Bot is missing `manage_roles` permissions" # ) - await channel.send( + await ctx.maybe_send_embed( f"{member.display_name} has been added to the game, " f"total players is **{len(self.players)}**" ) @@ -645,15 +649,15 @@ class Game: player = await self.get_player_by_member(ctx.author) if player is None: - await ctx.send("You're not in this game!") + await ctx.maybe_send_embed("You're not in this game!") return if not player.alive: - await ctx.send("**Corpses** can't participate...") + await ctx.maybe_send_embed("**Corpses** can't participate...") return if player.role.blocked: - await ctx.send("Something is preventing you from doing this...") + await ctx.maybe_send_embed("Something is preventing you from doing this...") return # Let role do target validation, might be alternate targets @@ -821,7 +825,7 @@ class Game: 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") + await ctx.maybe_send_embed("Code has been set") async def get_roles(self, ctx, game_code=None): if game_code is not None: @@ -833,10 +837,12 @@ class Game: try: self.roles = await parse_code(self.game_code, self) except ValueError as e: - await ctx.send("Invalid Code: Code contains unknown character\n{}".format(e)) + await ctx.maybe_send_embed( + "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)) + await ctx.maybe_send_embed("Invalid Code: Code references unknown role\n{}".format(e)) if not self.roles: return False @@ -898,7 +904,8 @@ class Game: self.game_over = True alignment1 = alive_players[0].role.alignment alignment2 = alive_players[1].role.alignment - if alignment1 == alignment2: # Same team + # Same team and not neutral + if alignment1 == alignment2 and alignment1 != ALIGNMENT_NEUTRAL: winners = alive_players else: winners = [max(alive_players, key=lambda p: p.role.alignment)] diff --git a/werewolf/player.py b/werewolf/player.py index 48885a8..7aec179 100644 --- a/werewolf/player.py +++ b/werewolf/player.py @@ -35,9 +35,13 @@ class Player: async def send_dm(self, message): try: - await self.member.send(message) # Lets do embeds later + await self.member.send(message) # Lets ToDo embeds later except discord.Forbidden: + log.info(f"Unable to mention {self.member.__repr__()}") await self.role.game.village_channel.send( f"Couldn't DM {self.mention}, uh oh", allowed_mentions=discord.AllowedMentions(users=[self.member]), ) + except AttributeError: + log.exception("Someone messed up and added a bot to the game (I think)") + await self.role.game.village_channel.send("Someone messed up and added a bot to the game :eyes:") diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index 599796c..dc27338 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -72,9 +72,9 @@ class Werewolf(Cog): code = await gb.build_game(ctx) if code != "": - await ctx.send(f"Your game code is **{code}**") + await ctx.maybe_send_embed(f"Your game code is **{code}**") else: - await ctx.send("No code generated") + await ctx.maybe_send_embed("No code generated") @checks.guildowner() @commands.group() @@ -117,10 +117,10 @@ class Werewolf(Cog): """ if role is None: await self.config.guild(ctx.guild).role_id.set(None) - await ctx.send("Cleared Game Role") + await ctx.maybe_send_embed("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)) + await ctx.maybe_send_embed("Game Role has been set to **{}**".format(role.name)) @commands.guild_only() @wwset.command(name="category") @@ -130,14 +130,14 @@ class Werewolf(Cog): """ if category_id is None: await self.config.guild(ctx.guild).category_id.set(None) - await ctx.send("Cleared Game Channel Category") + await ctx.maybe_send_embed("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") + await ctx.maybe_send_embed("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)) + await ctx.maybe_send_embed("Game Channel Category has been set to **{}**".format(category.name)) @commands.guild_only() @wwset.command(name="channel") @@ -147,10 +147,10 @@ class Werewolf(Cog): """ if channel is None: await self.config.guild(ctx.guild).channel_id.set(None) - await ctx.send("Cleared Game Channel") + await ctx.maybe_send_embed("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)) + await ctx.maybe_send_embed("Game Channel has been set to **{}**".format(channel.mention)) @commands.guild_only() @wwset.command(name="logchannel") @@ -160,10 +160,10 @@ class Werewolf(Cog): """ if channel is None: await self.config.guild(ctx.guild).log_channel_id.set(None) - await ctx.send("Cleared Game Log Channel") + await ctx.maybe_send_embed("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)) + await ctx.maybe_send_embed("Game Log Channel has been set to **{}**".format(channel.mention)) @commands.group() async def ww(self, ctx: commands.Context): @@ -181,9 +181,9 @@ class Werewolf(Cog): """ game = await self._get_game(ctx, game_code) if not game: - await ctx.send("Failed to start a new game") + await ctx.maybe_send_embed("Failed to start a new game") else: - await ctx.send("Game is ready to join! Use `[p]ww join`") + await ctx.maybe_send_embed("Game is ready to join! Use `[p]ww join`") @commands.guild_only() @ww.command(name="join") @@ -195,10 +195,27 @@ class Werewolf(Cog): game: Game = await self._get_game(ctx) if not game: - await ctx.send("No game to join!\nCreate a new one with `[p]ww new`") + await ctx.maybe_send_embed("Failed to join a game!") return - await game.join(ctx.author, ctx.channel) + await game.join(ctx, ctx.author) + await ctx.tick() + + @commands.guild_only() + @commands.admin() + @ww.command(name="forcejoin") + async def ww_forcejoin(self, ctx: commands.Context, target: discord.Member): + """ + Force someone to join a game of Werewolf + """ + + game: Game = await self._get_game(ctx) + + if not game: + await ctx.maybe_send_embed("Failed to join a game!") + return + + await game.join(ctx, target) await ctx.tick() @commands.guild_only() @@ -213,7 +230,7 @@ class Werewolf(Cog): game = await self._get_game(ctx) if not game: - await ctx.send("No game to join!\nCreate a new one with `[p]ww new`") + await ctx.maybe_send_embed("No game to join!\nCreate a new one with `[p]ww new`") return await game.set_code(ctx, code) @@ -239,7 +256,7 @@ class Werewolf(Cog): """ game = await self._get_game(ctx) if not game: - await ctx.send("No game running, cannot start") + await ctx.maybe_send_embed("No game running, cannot start") if not await game.setup(ctx): pass # ToDo something? @@ -257,13 +274,13 @@ class Werewolf(Cog): # await ctx.send("Cannot stop 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") + await ctx.maybe_send_embed("No game to stop") return game = await self._get_game(ctx) game.game_over = True await game.current_action.cancel() - await ctx.send("Game has been stopped") + await ctx.maybe_send_embed("Game has been stopped") @commands.guild_only() @ww.command(name="vote") @@ -277,7 +294,7 @@ class Werewolf(Cog): target_id = None if target_id is None: - await ctx.send("`id` must be an integer") + await ctx.maybe_send_embed("`id` must be an integer") return # if ctx.guild is None: @@ -294,7 +311,7 @@ class Werewolf(Cog): game = await self._get_game(ctx) if game is None: - await ctx.send("No game running, cannot vote") + await ctx.maybe_send_embed("No game running, cannot vote") return # Game handles response now @@ -304,7 +321,7 @@ class Werewolf(Cog): elif channel in (c["channel"] for c in game.p_channels.values()): await game.vote(ctx.author, target_id, channel) else: - await ctx.send("Nothing to vote for in this channel") + await ctx.maybe_send_embed("Nothing to vote for in this channel") @ww.command(name="choose") async def ww_choose(self, ctx: commands.Context, data): @@ -315,7 +332,7 @@ class Werewolf(Cog): """ if ctx.guild is not None: - await ctx.send("This action is only available in DM's") + await ctx.maybe_send_embed("This action is only available in DM's") return # DM nonsense, find their game # If multiple games, panic @@ -323,7 +340,7 @@ class Werewolf(Cog): if await game.get_player_by_member(ctx.author): break # game = game else: - await ctx.send("You're not part of any werewolf game") + await ctx.maybe_send_embed("You're not part of any werewolf game") return await game.choose(ctx, data) @@ -344,7 +361,7 @@ class Werewolf(Cog): if from_name: await menu(ctx, from_name, DEFAULT_CONTROLS) else: - await ctx.send("No roles containing that name were found") + await ctx.maybe_send_embed("No roles containing that name were found") @ww_search.command(name="alignment") async def ww_search_alignment(self, ctx: commands.Context, alignment: int): @@ -354,7 +371,7 @@ class Werewolf(Cog): if from_alignment: await menu(ctx, from_alignment, DEFAULT_CONTROLS) else: - await ctx.send("No roles with that alignment were found") + await ctx.maybe_send_embed("No roles with that alignment were found") @ww_search.command(name="category") async def ww_search_category(self, ctx: commands.Context, category: int): @@ -364,7 +381,7 @@ class Werewolf(Cog): if pages: await menu(ctx, pages, DEFAULT_CONTROLS) else: - await ctx.send("No roles in that category were found") + await ctx.maybe_send_embed("No roles in that category were found") @ww_search.command(name="index") async def ww_search_index(self, ctx: commands.Context, idx: int): @@ -374,23 +391,29 @@ class Werewolf(Cog): if idx_embed is not None: await ctx.send(embed=idx_embed) else: - await ctx.send("Role ID not found") + await ctx.maybe_send_embed("Role ID not found") async def _get_game(self, ctx: commands.Context, game_code=None) -> Union[Game, None]: guild: discord.Guild = getattr(ctx, "guild", None) if guild is None: # Private message, can't get guild - await ctx.send("Cannot start game from DM!") + await ctx.maybe_send_embed("Cannot start game from DM!") return None if guild.id not in self.games or self.games[guild.id].game_over: - await ctx.send("Starting a new game...") + await ctx.maybe_send_embed("Starting a new game...") valid, role, category, channel, log_channel = await self._get_settings(ctx) if not valid: - await ctx.send("Cannot start a new game") + await ctx.maybe_send_embed("Cannot start a new game") return None + who_has_the_role = await anyone_has_role(guild.members, role) + if who_has_the_role: + await ctx.maybe_send_embed( + f"Cannot continue, {who_has_the_role.display_name} already has the game role." + ) + return None self.games[guild.id] = Game( self.bot, guild, role, category, channel, log_channel, game_code )