import asyncio import typing as t import discord from discord.ext.commands import Cog, Context, check, command, guild_only from bot.bot import SeasonalBot from bot.constants import Emojis CONFIRMATION_MESSAGE = ( "{opponent}, {requester} want to play Tic-Tac-Toe against you. React to this message with " f"{Emojis.confirmation} to accept or with {Emojis.decline} to decline." ) class Player: """Class that contains information about player and functions that interact with player.""" def __init__(self, user: discord.User, ctx: Context, symbol: str): self.user = user self.ctx = ctx self.symbol = symbol class Game: """Class that contains information and functions about Tic Tac Toe game.""" def __init__(self, channel: discord.TextChannel, players: t.List[Player], ctx: Context): self.channel = channel self.players = players self.ctx = ctx self.board = [ [Emojis.number_emojis[1], Emojis.number_emojis[2], Emojis.number_emojis[3]], [Emojis.number_emojis[4], Emojis.number_emojis[5], Emojis.number_emojis[6]], [Emojis.number_emojis[7], Emojis.number_emojis[8], Emojis.number_emojis[9]] ] self.current = self.players[0] self.next = self.players[1] self.winner: t.Optional[Player] = None self.loser: t.Optional[Player] = None self.over = False async def get_confirmation(self) -> t.Tuple[bool, t.Optional[str]]: """Ask does user want to play TicTacToe against requester. First player is always requester.""" confirm_message = await self.ctx.send( CONFIRMATION_MESSAGE.format( opponent=self.players[1].user.mention, requester=self.players[0].user.mention ) ) await confirm_message.add_reaction(Emojis.confirmation) await confirm_message.add_reaction(Emojis.decline) def confirm_check(reaction: discord.Reaction, user: discord.User) -> bool: return ( reaction.emoji in (Emojis.confirmation, Emojis.decline) and reaction.message.id == confirm_message.id and user == self.players[1].user ) try: reaction, user = await self.ctx.bot.wait_for( "reaction_add", timeout=60.0, check=confirm_check ) except asyncio.TimeoutError: self.over = True await confirm_message.delete() return False, "Running out of time... Cancelled game." await confirm_message.delete() if reaction.emoji == Emojis.confirmation: return True, None else: self.over = True return False, "User declined" async def add_reactions(self, msg: discord.Message) -> None: """Add number emojis to message.""" for nr in Emojis.number_emojis.values(): await msg.add_reaction(nr) async def send_board(self) -> discord.Message: """Send board and return it's message.""" msg = "" for line in self.board: msg += " ".join(line) msg += "\n" message = await self.ctx.send(msg) return message def is_channel_free() -> t.Callable: """Check is channel where command will be invoked free.""" async def predicate(ctx: Context) -> bool: return all(game.channel != ctx.channel for game in ctx.cog.games if not game.over) return check(predicate) def is_requester_free() -> t.Callable: """Check is requester not already in any game.""" async def predicate(ctx: Context) -> bool: return all( ctx.author not in (player.user for player in game.players) for game in ctx.cog.games if not game.over ) return check(predicate) class TicTacToe(Cog): """TicTacToe cog contains tic-tac-toe game commands.""" def __init__(self, bot: SeasonalBot): self.bot = bot self.games: t.List[Game] = [] @guild_only() @is_channel_free() @is_requester_free() @command(name="tictactoe", aliases=("ttt",)) async def tic_tac_toe(self, ctx: Context, opponent: discord.User) -> None: """Tic Tac Toe game. Play agains friends. Use reactions to add your mark to field.""" if not all( opponent not in (player.user for player in g.players) for g in ctx.cog.games if not g.over ): await ctx.send("Opponent is already in game.") return game = Game( ctx.channel, [Player(ctx.author, ctx, Emojis.x), Player(opponent, ctx, Emojis.o)], ctx ) self.games.append(game) confirmed, msg = await game.get_confirmation() if not confirmed: if msg: await ctx.send(msg) return def setup(bot: SeasonalBot) -> None: """Load TicTacToe Cog.""" bot.add_cog(TicTacToe(bot))