class ConnectFour(Cog): """Play a game of Connect Four See `[p]help c4` manual for individual commands for more information. The classic game of Connect Four. Use these commands to play a game of Connect Four with another user. You can have multiple concurrent games, one per channel.""" def __init__(self, bot: Bot): self.bot = bot self.config = SubRedis(bot.db, "c4") self.sessions = dict() self.timeout = 120 self.timeout_incr = 1 # Valid reactions self._game_reactions = [ str(Emoji.one), str(Emoji.two), str(Emoji.three), str(Emoji.four), str(Emoji.five), str(Emoji.six), str(Emoji.seven), str(Emoji.x), str(Emoji.refresh) ] # Default chips self.empty_chip = AWBW_EMOJIS["ne"] self.p1_chip = AWBW_EMOJIS["os"] self.p2_chip = AWBW_EMOJIS["bm"] def session(self, channel: TextChannel) -> Optional[ConnectFourSession]: """Returns an active ConnectFourSession if there is a running game in a channel""" return self.sessions.get(channel.id, None) def channel_check(self, channel: TextChannel) -> bool: """Returns true if C4 is enabled in channel""" return str(channel.id) in self.config.smembers("allowed_channels") @staticmethod async def member_check(ctx: Context, member: str) -> Optional[Member]: """Attempt to convert an argument an argument to :class:`Member`""" try: return await MemberConverter().convert(ctx, member) except (BadArgument, NoPrivateMessage, TypeError): return None @staticmethod def get_member_chip(roles: List[Role], skip: str = None) -> Tuple[str, str]: for ctry, role_id in AWBW_ROLES.items(): if role_id in [r.id for r in roles] and ctry != skip: return ctry, AWBW_EMOJIS[ctry] else: ctry = choice( [ctry for ctry in AWBW_EMOJIS.keys() if ctry != skip]) return ctry, AWBW_EMOJIS[ctry] @staticmethod async def send_message(channel: TextChannel, msg: str = "Error", level: int = MsgLevel.info) -> None: """Formats a message as embed""" em = Embed(description=f"{MSG_ICON[level.value]} {msg}", color=MSG_COLOR[level.value]) msg = await channel.send(embed=em) await sleep(5) await msg.delete() async def init_game_message(self, channel: TextChannel, session: ConnectFourSession, board: Embed) -> Message: """Sends game board to channel and sets message on session""" msg = await channel.send(embed=board) for react in self._game_reactions: await msg.add_reaction(str(react)) await sleep(0.1) session.msg = msg return msg async def send_board(self, channel: TextChannel) -> Message: """Prepare game Embed and update message""" session = self.session(channel) em = Embed( title= f"{session.p1.chip}{session.p1.name} {Emoji.vs} {session.p2.name}{session.p2.chip}", description=f"\n" f"{Emoji.one}{Emoji.two}{Emoji.three}{Emoji.four}{Emoji.five}{Emoji.six}{Emoji.seven}\n" f"{session.draw_board}") if session.state == State.init: em.description = f"New game! Turn: 1\n" \ f"{em.description}\n" \ f"\n" \ f"{session.current_player.mention}'s turn: {session.current_player.chip}" em.colour = session.colour return await self.init_game_message(channel, session, em) elif session.state == State.active: em.description = f"Turn: {(session.turn + 2) // 2}\n" \ f"{em.description}\n" \ f"\n" \ f"{session.current_player.mention}'s turn: {session.current_player.chip}" em.colour = session.colour elif session.state == State.draw: self.sessions.pop(channel.id) em.description = f"Game ended in a Draw.\n" \ f"{em.description}" em.colour = Colour.dark_grey() await session.msg.clear_reactions() elif session.state == State.forfeit: self.sessions.pop(channel.id) em.description = f"Game Over. {session.current_player.name} Forfeits.\n" \ f"{em.description}" em.colour = Colour.dark_grey() await session.msg.clear_reactions() elif session.state == State.timeout: self.sessions.pop(channel.id) em.description = f"Time Out. {session.current_player.name} Forfeits.\n" \ f"{em.description}" em.colour = Colour.dark_grey() await session.msg.clear_reactions() elif session.state == State.won: self.sessions.pop(channel.id) em.description = f"Game Over!\n{session.current_player.name} wins! {Emoji.tada}\n" \ f"{em.description}" em.colour = 0xFDFF00 await session.msg.clear_reactions() # TODO: check I can edit the message (retrievable), if not, init message return await session.msg.edit(embed=em) @group(name="c4", invoke_without_command=True) async def c4(self, ctx: Context, *, member=None): """Connect Four `[p]c4 @user` to start a game with another user in the current channel.""" if not member: return await self.bot.help_command.send_help_for( ctx, ctx.command, "You need another player to start") try: member = await self.member_check(ctx, member) except BadArgument: return await self.member_check(ctx, member) if not self.channel_check(ctx.channel): return await self.send_message( ctx.channel, msg= f"{ctx.author.mention}: Connect Four is not enabled in this channel", level=MsgLevel.error) elif self.session(ctx.channel): return await self.send_message( ctx.channel, msg= f"{ctx.author.mention}: There is already an active session in this channel", level=MsgLevel.warning) elif member.id == ctx.author.id: return await self.send_message( ctx.channel, msg= f"{ctx.author.mention}: You cannot start a game with yourself.", level=MsgLevel.warning) elif member.bot: return await self.send_message( ctx.channel, msg=f"{ctx.author.mention}: You cannot play against bots.", level=MsgLevel.warning) elif member.id == ctx.author.id: return await self.send_message( ctx.channel, msg= f"{ctx.author.mention}: You cannot start a game with yourself.", level=MsgLevel.warning) else: p2_ctry, p2_chip = self.get_member_chip(ctx.author.roles) _, p1_chip = self.get_member_chip(member.roles, p2_ctry) self.sessions[ctx.channel.id] = ConnectFourSession( p1=member, p1_chip=p1_chip, p2=ctx.author, p2_chip=p2_chip, empty=self.empty_chip) await self.send_board(ctx.channel) @c4.command(name="help", hidden=True) async def c4_help(self, ctx): """Shortcut to send help manual for ConnectFour""" await self.bot.help_command.send_help_for( ctx, self.bot.get_cog("ConnectFour")) @c4.command(name="board") async def c4_board(self, ctx: Context): """Resend the current game board""" session = self.session(ctx.channel) if not session: return await self.send_message( ctx.channel, msg= f"{ctx.author.mention} There is no active game in this channel.", level=MsgLevel.warning) elif ctx.author.id not in session.players.keys(): return else: await self.init_game_message(ctx.channel, session, session.msg.embeds[0]) @sudo() @c4.command(name="enable", hidden=True) async def c4_enable(self, ctx: Context, *, chan: TextChannel = None): """Enable Connect Four on a channel Run without an argument to enable on the current channel Pass a channel as an argument to enable on that channel""" if not chan: chan = ctx.channel if self.config.sadd("allowed_channels", chan.id): await self.send_message( ctx.channel, msg="Connect Four successfully enabled on channel.", level=MsgLevel.info) else: await self.send_message( ctx.channel, msg="Connect Four already enabled on channel.", level=MsgLevel.warning) @sudo() @c4.command(name="disable", hidden=True) async def c4_disable(self, ctx: Context, *, chan: TextChannel = None): """Disable Connect Four on a channel Run without an argument to disabled on the current channel Pass a channel as an argument to disable on that channel""" if not chan: chan = ctx.channel if self.config.srem("allowed_channels", chan.id): await self.send_message( ctx.channel, msg="Connect Four successfully disabled on channel.", level=MsgLevel.info) else: await self.send_message( ctx.channel, msg="Connect Four already disabled on channel.", level=MsgLevel.warning) @sudo() @c4.command(name="games", hidden=True) async def c4_games(self, ctx: Context): """Shows the number of running sessions""" sessions = '\n'.join([str(s) for s in self.sessions.values()]) await self.send_message( ctx.channel, msg=f"Total running games: {len(self.sessions.keys())}\n" f"\n" f"{sessions}", level=MsgLevel.info) @sudo() @c4.command(name="kill", hidden=True) async def c4_kill(self, ctx: Context, *, _all: bool = False): """Administrative kill command This will kill a running game in the current channel""" if not _all: if self.sessions.pop(ctx.channel.id, None): await self.send_message( ctx.channel, msg="Current game in this channel has been terminated.", level=MsgLevel.info) else: await self.send_message(ctx.channel, msg="No active game in this channel.", level=MsgLevel.warning) else: await self.send_message( ctx.channel, msg= f"All running games have been terminated. (Total: {len(self.sessions.keys())})", level=MsgLevel.info) self.sessions = dict() @sudo() @c4.command(name="killall", hidden=True) async def c4_killall(self, ctx: Context): """Administrative kill command This will kill all running games in all channels""" ctx.message.content = f"{self.bot.command_prefix}kill True" await self.bot.process_commands(ctx.message) @Cog.listener(name="on_reaction_add") async def on_reaction_add(self, reaction: Reaction, user: Union[User, Member]): await sleep(0.1) if not self.channel_check(reaction.message.channel): # Channel is not watched for Connect Four return if user.id == self.bot.user.id: # Ignore the bot's own reactions return # It's a watched channel, so try to get an active session session = self.session(reaction.message.channel) if not session: # No session in channel return if reaction.message.id != session.msg.id: # Message is not an active session return if reaction.emoji not in self._game_reactions: # Not a valid game reaction return # It is a valid game reaction on an active session in a watched channel # Remove reaction, then act based on reaction await reaction.remove(user) if user.id not in session.players: # Not a player return # Currently the other player's turn if user.id != session.current_player.id: # Still allow quitting if reaction.emoji == str(Emoji.x): session.turn -= 1 session.forfeit() return await self.send_board(reaction.message.channel) # And resending the board elif reaction.emoji == str(Emoji.refresh): await session.msg.delete() return await self.init_game_message(reaction.message.channel, session, session.msg.embeds[0]) # Otherwise needs to be player's turn else: return await self.send_message( reaction.message.channel, msg=f"{user.mention}: It is not your turn.", level=MsgLevel.warning) # Resend the current game board if reaction.emoji == str(Emoji.refresh): await session.msg.delete() return await self.init_game_message(reaction.message.channel, session, session.msg.embeds[0]) if reaction.emoji == str(Emoji.one): try: session.play(user, 1) except ValueError: await self.send_message( reaction.message.channel, msg="That column is full. Select another.", level=MsgLevel.error) return await self.send_board(reaction.message.channel) if reaction.emoji == str(Emoji.two): try: session.play(user, 2) except ValueError: await self.send_message( reaction.message.channel, msg="That column is full. Select another.", level=MsgLevel.error) return await self.send_board(reaction.message.channel) if reaction.emoji == str(Emoji.three): try: session.play(user, 3) except ValueError: await self.send_message( reaction.message.channel, msg="That column is full. Select another.", level=MsgLevel.error) return await self.send_board(reaction.message.channel) if reaction.emoji == str(Emoji.four): try: session.play(user, 4) except ValueError: await self.send_message( reaction.message.channel, msg="That column is full. Select another.", level=MsgLevel.error) return await self.send_board(reaction.message.channel) if reaction.emoji == str(Emoji.five): try: session.play(user, 5) except ValueError: await self.send_message( reaction.message.channel, msg="That column is full. Select another.", level=MsgLevel.error) return await self.send_board(reaction.message.channel) if reaction.emoji == str(Emoji.six): try: session.play(user, 6) except ValueError: await self.send_message( reaction.message.channel, msg="That column is full. Select another.", level=MsgLevel.error) return await self.send_board(reaction.message.channel) if reaction.emoji == str(Emoji.seven): try: session.play(user, 7) except ValueError: await self.send_message( reaction.message.channel, msg="That column is full. Select another.", level=MsgLevel.error) return await self.send_board(reaction.message.channel) if reaction.emoji == str(Emoji.x): session.forfeit() return await self.send_board(reaction.message.channel) @Cog.listener(name="on_timer_update") async def on_timer_update(self, sec: int): """Timer event that triggers every 1 second""" # Only check statuses every `self.timeout_incr` seconds if sec % self.timeout_incr == 0: # Get all sessions and start a list of ones that timed out sessions = self.sessions.values() to_expire = list() # Increment the time on all for session in sessions: session.timeout += self.timeout_incr # Timeout reached # Add to list of expired sessions if session.timeout == self.timeout: to_expire.append(session) # Set game over condition on all expired sessions to timeout and send for session in to_expire: session.expire() await self.send_board(session.msg.channel)
class SelfRoles(Cog): """Commands for managing and assigning selfroles""" def __init__(self, bot: Bot): self.bot = bot self.config = SubRedis(bot.db, "selfroles") self.errorlog = bot.errorlog self._selfroles = {g.id: [] for g in bot.guilds} for key in self.config.scan_iter(match=f"{self.config}:*"): guild = bot.get_guild(int(key.split(":")[-1])) if not guild: continue self._selfroles[guild.id] = [] r_ids = [int(r_id) for r_id in self.config.smembers(key)] for role in guild.roles: if role.id in r_ids: self._selfroles[guild.id].append(role.id) @guild_only() @command(name="iam") async def iam(self, ctx: Context, *, role) -> None: """Add a self-assignable role If the `role` is configured as an assignable selfrole, you can use this command to assign the role to yourself. `[p]iam role` `[p]iam @role` `[p]iam role id` You can also use a country's short code. e.g. `[p]iam os`""" if role.lower() in ROLE_SHORTNAME.keys(): role = str(ROLE_SHORTNAME[role.lower()]) try: role = await RoleConverter().convert(ctx, role) except BadArgument: await ctx.send( embed=Embed(color=ctx.guild.me.colour, title="⚠ Selfroles", description=f"Could not recognize role.")) else: if role in ctx.author.roles: await ctx.send(embed=Embed( color=ctx.guild.me.colour, title="⚠ Selfroles", description="You already have this role assigned.")) elif role.id in self._selfroles.get(ctx.guild.id): for author_role in ctx.author.roles: if author_role.id in self._selfroles[ctx.guild.id]: await ctx.author.remove_roles( author_role, reason=ctx.message.content, atomic=True) await sleep(0.5) await ctx.author.add_roles(role, reason=ctx.message.content, atomic=True) await ctx.send(embed=Embed( color=role.colour, title="Role Assigned", description=f"Congratulations, {ctx.author.mention}!" f" You now have the **{role.mention}** " f"role.")) else: await ctx.send(embed=Embed( color=0xFF0000, title="⚠ Selfroles", description="That role is not self-assignable.")) @guild_only() @group(name="selfroles", aliases=["selfrole"], invoke_without_command=True) async def selfroles(self, ctx: Context): """View all selfroles""" r_ids = self._selfroles.get(ctx.guild.id) if r_ids: roles = [] for role in ctx.guild.roles: if role.id in r_ids: roles.append(role.mention) roles = "\n".join(roles) await ctx.send(embed=Embed( color=ctx.guild.me.colour, title="Selfroles", description=f"The following roles are self-assignable:\n" f"{roles}" f"\n" f"\n" f"See `?help iam` for assigning selfroles.")) else: await ctx.send(embed=Embed( color=ctx.guild.me.colour, title="Selfroles", description="There are currently no assignable selfroles.\n" "Staff may configure selfroles with `?selfrole add`.")) @awbw_staff() @selfroles.command(name="add") async def _add(self, ctx: Context, *, role) -> None: """Configures a role as a selfrole""" r_ids = [i for i in self._selfroles.get(ctx.guild.id)] try: role = await RoleConverter().convert(ctx, role) except BadArgument: await ctx.send( embed=Embed(color=ctx.guild.me.colour, title="⚠ Selfroles Management", description=f"Could not recognize role.")) else: if role.id in r_ids: await ctx.send(embed=Embed( color=ctx.guild.me.colour, title="⚠ Selfroles Management", description= f"Role {role.mention} is already configured as a selfrole." )) else: self.config.sadd(f"{self.config}:{ctx.guild.id}", str(role.id)) self._selfroles[ctx.guild.id].append(role.id) await ctx.send(embed=Embed( color=ctx.guild.me.colour, title="Selfroles Management", description= f"Role {role.mention} has been added to selfroles.")) @awbw_staff() @selfroles.command(name="rem", aliases=["del", "remove", "delete"]) async def _rem(self, ctx: Context, *, role): """Removes a role from selfroles""" r_ids = [i for i in self._selfroles.get(ctx.guild.id)] try: role = await RoleConverter().convert(ctx, role) except BadArgument: await ctx.send( embed=Embed(color=ctx.guild.me.colour, title="⚠ Selfroles Management", description=f"Could not recognize role.")) else: if role.id in r_ids: self.config.srem(f"{self.config}:{ctx.guild.id}", role.id) self._selfroles[ctx.guild.id].remove(role.id) await ctx.send(embed=Embed( color=ctx.guild.me.colour, title="Selfroles Management", description= f"Role {role.mention} has been removed from selfroles.")) else: await ctx.send(embed=Embed( color=ctx.guild.me.colour, title="⚠ Selfroles Management", description= f"Role {role.mention} is not configured as a selfrole."))
class Poll(Cog): """User-created polls""" def __init__(self, bot: Bot): self.bot = bot self.config = SubRedis(bot.db, "poll") self.errorlog = bot.errorlog @check(has_permission) @group(name="poll", invoke_without_command=True) async def poll(self, ctx: Context): await ctx.send("Placeholder") """ ####### Setup ####### """ @check(has_permission) @poll.command(name="view") async def view(self, ctx: Context): pass @check(has_permission) @poll.command(name="create", aliases=["new"]) async def create(self, ctx: Context, *, name: str): pass @check(has_permission) @poll.command(name="discard", aliases=["drop"]) async def discard(self, ctx: Context): pass @check(has_permission) @poll.command(name="name", aliases=["title"]) async def name(self, ctx: Context): pass @check(has_permission) @poll.command(name="add_option", aliases=["add"]) async def add_option(self, ctx: Context): pass @check(has_permission) @poll.command(name="remove_option", aliases=["remove", "delete_option", "delete", "rem", "del"]) async def remove_option(self, ctx: Context): pass """ ######### Running ######### """ @check(has_permission) @poll.command(name="start") async def start(self, ctx: Context, *, channel: TextChannel): pass @check(has_permission) @poll.command(name="view") async def view(self, ctx: Context): pass """ ################ Administrative ################ """ @has_guild_permissions(manage_guild=True) @poll.command(name="add_role", hidden=True) async def add_role(self, ctx: Context, role: Role): if self.config.sismember(f"allowed_roles:{ctx.guild.id}", role.id): em = Embed(title="Polls Administration", description= f"{role.mention} is already allowed to conduct polls.", color=Color.red()) await ctx.send(embed=em, delete_after=5) else: self.config.sadd(f"allowed_roles:{ctx.guild.id}", role.id) em = Embed( title="Polls Administration", description=f"{role.mention} is now allowed to conduct polls.", color=Color.green()) await ctx.send(embed=em, delete_after=5) @has_guild_permissions(manage_guild=True) @poll.command(name="remove_role", aliases=["rem_role"], hidden=True) async def remove_role(self, ctx: Context, role: Role): if self.config.sismember(f"allowed_roles:{ctx.guild.id}", role.id): self.config.srem(f"allowed_roles:{ctx.guild.id}", role.id) em = Embed( title="Polls Administration", description= f"{role.mention} is no longer allowed to conduct polls.", color=Color.green()) await ctx.send(embed=em, delete_after=5) else: em = Embed( title="Polls Administration", description=f"{role.mention} is not allowed to conduct polls.", color=Color.red()) await ctx.send(embed=em, delete_after=5) @has_guild_permissions(manage_guild=True) @poll.command(name="list_roles", aliases=["roles"], hidden=True) async def list_roles(self, ctx: Context): role_ids = sorted( self.config.smembers(f"allowed_roles:{ctx.guild.id}")) if role_ids: roles = "\n".join([ ctx.guild.get_role(int(role_id)).mention for role_id in role_ids ]) em = Embed( title="Polls Administration", description= f"The following roles are allowed to conduct polls:\n{roles}", color=Color.green()) await ctx.send(embed=em, delete_after=5) else: em = Embed( title="Polls Administration", description= "There are currently no roles allowed to conduct polls.", color=Color.red()) await ctx.send(embed=em, delete_after=5)