import logging from typing import List, Union import discord from redbot.core import Config, checks, commands from redbot.core.bot import Red from redbot.core.commands import Cog from redbot.core.utils import AsyncIter from redbot.core.utils.menus import DEFAULT_CONTROLS, menu from werewolf.builder import ( GameBuilder, role_from_alignment, role_from_category, role_from_id, role_from_name, ) from werewolf.game import Game log = logging.getLogger("red.fox_v3.werewolf") async def anyone_has_role( member_list: List[discord.Member], role: discord.Role ) -> Union[None, discord.Member]: return await AsyncIter(member_list).find( lambda m: AsyncIter(m.roles).find(lambda r: r.id == role.id) ) class Werewolf(Cog): """ Base to host werewolf on a guild """ def __init__(self, bot: Red): super().__init__() self.bot = bot self.config = Config.get_conf( self, identifier=87101114101119111108102, force_registration=True ) default_global = {} default_guild = { "role_id": None, "category_id": None, "channel_id": None, "log_channel_id": None, } self.config.register_global(**default_global) self.config.register_guild(**default_guild) self.games = {} # Active games stored here, id is per guild async def red_delete_data_for_user(self, **kwargs): """Nothing to delete""" return def cog_unload(self): log.debug("Unload called") for game in self.games.values(): del game @commands.command() async def buildgame(self, ctx: commands.Context): """ Create game codes to run custom games. Pick the roles or randomized roles you want to include in a game """ gb = GameBuilder() code = await gb.build_game(ctx) if code != "": await ctx.maybe_send_embed(f"Your game code is **{code}**") else: await ctx.maybe_send_embed("No code generated") @checks.guildowner() @commands.group() async def wwset(self, ctx: commands.Context): """ Base command to adjust settings. Check help for command list. """ if ctx.invoked_subcommand is None: pass @commands.guild_only() @wwset.command(name="list") async def wwset_list(self, ctx: commands.Context): """ Lists current guild settings """ valid, role, category, channel, log_channel = await self._get_settings(ctx) # if not valid: # await ctx.send("Failed to get settings") # return None embed = discord.Embed( title="Current Guild Settings", description=f"Valid: {valid}", color=0x008000 if valid else 0xFF0000, ) 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: commands.Context, role: discord.Role = None): """ Set the game role This role should not be manually assigned """ if role is None: await self.config.guild(ctx.guild).role_id.set(None) await ctx.maybe_send_embed("Cleared Game Role") else: await self.config.guild(ctx.guild).role_id.set(role.id) await ctx.maybe_send_embed("Game Role has been set to **{}**".format(role.name)) @commands.guild_only() @wwset.command(name="category") async def wwset_category(self, ctx: commands.Context, category_id: int = None): """ Assign the channel category """ if category_id is None: await self.config.guild(ctx.guild).category_id.set(None) 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.maybe_send_embed("Category not found") return await self.config.guild(ctx.guild).category_id.set(category.id) await ctx.maybe_send_embed( "Game Channel Category has been set to **{}**".format(category.name) ) @commands.guild_only() @wwset.command(name="channel") async def wwset_channel(self, ctx: commands.Context, channel: discord.TextChannel = None): """ Assign the village channel """ if channel is None: await self.config.guild(ctx.guild).channel_id.set(None) await ctx.maybe_send_embed("Cleared Game Channel") else: await self.config.guild(ctx.guild).channel_id.set(channel.id) await ctx.maybe_send_embed( "Game Channel has been set to **{}**".format(channel.mention) ) @commands.guild_only() @wwset.command(name="logchannel") async def wwset_log_channel(self, ctx: commands.Context, channel: discord.TextChannel = None): """ Assign the log channel """ if channel is None: await self.config.guild(ctx.guild).log_channel_id.set(None) await ctx.maybe_send_embed("Cleared Game Log Channel") else: await self.config.guild(ctx.guild).log_channel_id.set(channel.id) await ctx.maybe_send_embed( "Game Log Channel has been set to **{}**".format(channel.mention) ) @commands.group() async def ww(self, ctx: commands.Context): """ Base command for this cog. Check help for the commands list. """ if ctx.invoked_subcommand is None: pass @commands.guild_only() @ww.command(name="new") async def ww_new(self, ctx: commands.Context, game_code=None): """ Create and join a new game of Werewolf """ game = await self._get_game(ctx, game_code) if not game: await ctx.maybe_send_embed("Failed to start a new game") else: await ctx.maybe_send_embed("Game is ready to join! Use `[p]ww join`") @commands.guild_only() @ww.command(name="join") async def ww_join(self, ctx: commands.Context): """ Joins 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, 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() @ww.command(name="code") async def ww_code(self, ctx: commands.Context, code): """ Adjusts the game code. See `[p]buildgame` to generate a new code """ game = await self._get_game(ctx) if not game: await ctx.maybe_send_embed("No game to join!\nCreate a new one with `[p]ww new`") return await game.set_code(ctx, code) await ctx.tick() @commands.guild_only() @ww.command(name="quit") async def ww_quit(self, ctx: commands.Context): """ Quit a game of Werewolf """ game = await self._get_game(ctx) await game.quit(ctx.author, ctx.channel) await ctx.tick() @commands.guild_only() @ww.command(name="start") async def ww_start(self, ctx: commands.Context): """ Checks number of players and attempts to start the game """ game = await self._get_game(ctx) if not game: await ctx.maybe_send_embed("No game running, cannot start") if not await game.setup(ctx): pass # ToDo something? await ctx.tick() @commands.guild_only() @ww.command(name="stop") async def ww_stop(self, ctx: commands.Context): """ Stops the current game """ # if ctx.guild is None: # # Private message, can't get guild # 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.maybe_send_embed("No game to stop") return game = await self._get_game(ctx) game.game_over = True if game.current_action: game.current_action.cancel() await ctx.maybe_send_embed("Game has been stopped") @commands.guild_only() @ww.command(name="vote") async def ww_vote(self, ctx: commands.Context, target_id: int): """ Vote for a player by ID """ try: target_id = int(target_id) except ValueError: target_id = None if target_id is None: await ctx.maybe_send_embed("`id` must be an integer") return # if ctx.guild is None: # # DM nonsense, find their game # # If multiple games, panic # for game in self.games.values(): # if await game.get_player_by_member(ctx.author): # break #game = game # else: # await ctx.send("You're not part of any werewolf game") # return # else: game = await self._get_game(ctx) if game is None: await ctx.maybe_send_embed("No game running, cannot vote") return # Game handles response now channel = ctx.channel if channel == game.village_channel: await game.vote(ctx.author, target_id, channel) elif channel in (c["channel"] for c in game.p_channels.values()): await game.vote(ctx.author, target_id, channel) else: 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): """ Arbitrary decision making Handled by game+role Can be received by DM """ if ctx.guild is not None: await ctx.maybe_send_embed("This action is only available in DM's") return # DM nonsense, find their game # If multiple games, panic for game in self.games.values(): if await game.get_player_by_member(ctx.author): break # game = game else: await ctx.maybe_send_embed("You're not part of any werewolf game") return await game.choose(ctx, data) @ww.group(name="search") async def ww_search(self, ctx: commands.Context): """ Find custom roles by name, alignment, category, or ID """ if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self.ww_search: pass @ww_search.command(name="name") async def ww_search_name(self, ctx: commands.Context, *, name): """Search for a role by name""" if name is not None: from_name = role_from_name(name) if from_name: await menu(ctx, from_name, DEFAULT_CONTROLS) else: await ctx.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): """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.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): """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.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): """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.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.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.maybe_send_embed("Starting a new game...") valid, role, category, channel, log_channel = await self._get_settings(ctx) if not valid: 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 ) return self.games[guild.id] 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, role, 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, role, category, 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 ( role is not None and category is not None and channel is not None, role, category, channel, log_channel, )