diff options
Diffstat (limited to 'bot/exts/evergreen/tic_tac_toe.py')
-rw-r--r-- | bot/exts/evergreen/tic_tac_toe.py | 335 |
1 files changed, 0 insertions, 335 deletions
diff --git a/bot/exts/evergreen/tic_tac_toe.py b/bot/exts/evergreen/tic_tac_toe.py deleted file mode 100644 index 5c4f8051..00000000 --- a/bot/exts/evergreen/tic_tac_toe.py +++ /dev/null @@ -1,335 +0,0 @@ -import asyncio -import random -from typing import Callable, Optional, Union - -import discord -from discord.ext.commands import Cog, Context, check, group, guild_only - -from bot.bot import Bot -from bot.constants import Emojis -from bot.utils.pagination import LinePaginator - -CONFIRMATION_MESSAGE = ( - "{opponent}, {requester} wants to play Tic-Tac-Toe against you." - f"\nReact to this message with {Emojis.confirmation} to accept or with {Emojis.decline} to decline." -) - - -def check_win(board: dict[int, str]) -> bool: - """Check from board, is any player won game.""" - return any( - ( - # Horizontal - board[1] == board[2] == board[3], - board[4] == board[5] == board[6], - board[7] == board[8] == board[9], - # Vertical - board[1] == board[4] == board[7], - board[2] == board[5] == board[8], - board[3] == board[6] == board[9], - # Diagonal - board[1] == board[5] == board[9], - board[3] == board[5] == board[7], - ) - ) - - -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 - - async def get_move(self, board: dict[int, str], msg: discord.Message) -> tuple[bool, Optional[int]]: - """ - Get move from user. - - Return is timeout reached and position of field what user will fill when timeout don't reach. - """ - def check_for_move(r: discord.Reaction, u: discord.User) -> bool: - """Check does user who reacted is user who we want, message is board and emoji is in board values.""" - return ( - u.id == self.user.id - and msg.id == r.message.id - and r.emoji in board.values() - and r.emoji in Emojis.number_emojis.values() - ) - - try: - react, _ = await self.ctx.bot.wait_for("reaction_add", timeout=30.0, check=check_for_move) - except asyncio.TimeoutError: - return True, None - else: - return False, list(Emojis.number_emojis.keys())[list(Emojis.number_emojis.values()).index(react.emoji)] - - def __str__(self) -> str: - """Return mention of user.""" - return self.user.mention - - -class AI: - """Tic Tac Toe AI class for against computer gaming.""" - - def __init__(self, symbol: str): - self.symbol = symbol - - async def get_move(self, board: dict[int, str], _: discord.Message) -> tuple[bool, int]: - """Get move from AI. AI use Minimax strategy.""" - possible_moves = [i for i, emoji in board.items() if emoji in list(Emojis.number_emojis.values())] - - for symbol in (Emojis.o_square, Emojis.x_square): - for move in possible_moves: - board_copy = board.copy() - board_copy[move] = symbol - if check_win(board_copy): - return False, move - - open_corners = [i for i in possible_moves if i in (1, 3, 7, 9)] - if len(open_corners) > 0: - return False, random.choice(open_corners) - - if 5 in possible_moves: - return False, 5 - - open_edges = [i for i in possible_moves if i in (2, 4, 6, 8)] - return False, random.choice(open_edges) - - def __str__(self) -> str: - """Return `AI` as user name.""" - return "AI" - - -class Game: - """Class that contains information and functions about Tic Tac Toe game.""" - - def __init__(self, players: list[Union[Player, AI]], ctx: Context): - self.players = players - self.ctx = ctx - self.board = { - 1: Emojis.number_emojis[1], - 2: Emojis.number_emojis[2], - 3: Emojis.number_emojis[3], - 4: Emojis.number_emojis[4], - 5: Emojis.number_emojis[5], - 6: Emojis.number_emojis[6], - 7: Emojis.number_emojis[7], - 8: Emojis.number_emojis[8], - 9: Emojis.number_emojis[9] - } - - self.current = self.players[0] - self.next = self.players[1] - - self.winner: Optional[Union[Player, AI]] = None - self.loser: Optional[Union[Player, AI]] = None - self.over = False - self.canceled = False - self.draw = False - - async def get_confirmation(self) -> tuple[bool, Optional[str]]: - """ - Ask does user want to play TicTacToe against requester. First player is always requester. - - This return tuple that have: - - first element boolean (is game accepted?) - - (optional, only when first element is False, otherwise None) reason for declining. - """ - 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: - """Check is user who reacted from who this was requested, message is confirmation and emoji is valid.""" - 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 - self.canceled = 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 - self.canceled = 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) - - def format_board(self) -> str: - """Get formatted tic-tac-toe board for message.""" - board = list(self.board.values()) - return "\n".join( - (f"{board[line]} {board[line + 1]} {board[line + 2]}" for line in range(0, len(board), 3)) - ) - - async def play(self) -> None: - """Start and handle game.""" - await self.ctx.send("It's time for the game! Let's begin.") - board = await self.ctx.send( - embed=discord.Embed(description=self.format_board()) - ) - await self.add_reactions(board) - - for _ in range(9): - if isinstance(self.current, Player): - announce = await self.ctx.send( - f"{self.current.user.mention}, it's your turn! " - "React with an emoji to take your go." - ) - timeout, pos = await self.current.get_move(self.board, board) - if isinstance(self.current, Player): - await announce.delete() - if timeout: - await self.ctx.send(f"{self.current.user.mention} ran out of time. Canceling game.") - self.over = True - self.canceled = True - return - self.board[pos] = self.current.symbol - await board.edit( - embed=discord.Embed(description=self.format_board()) - ) - await board.clear_reaction(Emojis.number_emojis[pos]) - if check_win(self.board): - self.winner = self.current - self.loser = self.next - await self.ctx.send( - f":tada: {self.current} won this game! :tada:" - ) - await board.clear_reactions() - break - self.current, self.next = self.next, self.current - if not self.winner: - self.draw = True - await self.ctx.send("It's a DRAW!") - self.over = True - - -def is_channel_free() -> 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() -> 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): - self.games: list[Game] = [] - - @guild_only() - @is_channel_free() - @is_requester_free() - @group(name="tictactoe", aliases=("ttt", "tic"), invoke_without_command=True) - async def tic_tac_toe(self, ctx: Context, opponent: Optional[discord.User]) -> None: - """Tic Tac Toe game. Play against friends or AI. Use reactions to add your mark to field.""" - if opponent == ctx.author: - await ctx.send("You can't play against yourself.") - return - if opponent is not None and 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 - if opponent is None: - game = Game( - [Player(ctx.author, ctx, Emojis.x_square), AI(Emojis.o_square)], - ctx - ) - else: - game = Game( - [Player(ctx.author, ctx, Emojis.x_square), Player(opponent, ctx, Emojis.o_square)], - ctx - ) - self.games.append(game) - if opponent is not None: - if opponent.bot: # check whether the opponent is a bot or not - await ctx.send("You can't play Tic-Tac-Toe with bots!") - return - - confirmed, msg = await game.get_confirmation() - - if not confirmed: - if msg: - await ctx.send(msg) - return - await game.play() - - @tic_tac_toe.group(name="history", aliases=("log",), invoke_without_command=True) - async def tic_tac_toe_logs(self, ctx: Context) -> None: - """Show most recent tic-tac-toe games.""" - if len(self.games) < 1: - await ctx.send("No recent games.") - return - log_games = [] - for i, game in enumerate(self.games): - if game.over and not game.canceled: - if game.draw: - log_games.append( - f"**#{i+1}**: {game.players[0]} vs {game.players[1]} (draw)" - ) - else: - log_games.append( - f"**#{i+1}**: {game.winner} :trophy: vs {game.loser}" - ) - await LinePaginator.paginate( - log_games, - ctx, - discord.Embed(title="Most recent Tic Tac Toe games") - ) - - @tic_tac_toe_logs.command(name="show", aliases=("s",)) - async def show_tic_tac_toe_board(self, ctx: Context, game_id: int) -> None: - """View game board by ID (ID is possible to get by `.tictactoe history`).""" - if len(self.games) < game_id: - await ctx.send("Game don't exist.") - return - game = self.games[game_id - 1] - - if game.draw: - description = f"{game.players[0]} vs {game.players[1]} (draw)\n\n{game.format_board()}" - else: - description = f"{game.winner} :trophy: vs {game.loser}\n\n{game.format_board()}" - - embed = discord.Embed( - title=f"Match #{game_id} Game Board", - description=description, - ) - await ctx.send(embed=embed) - - -def setup(bot: Bot) -> None: - """Load the TicTacToe cog.""" - bot.add_cog(TicTacToe()) |