diff --git a/cogs/tictactoe.py b/cogs/tictactoe.py new file mode 100644 index 00000000..a44f868d --- /dev/null +++ b/cogs/tictactoe.py @@ -0,0 +1,137 @@ +from typing import List +import discord +from discord import app_commands +from discord.ext import commands + + +class TicTacToeCog(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @app_commands.command(name="play-tictactoe", description="Play Tic-Tac-Toe Game!") + async def play_tictactoe( + self, + inter: discord.Interaction, + ): + """Starts a tic-tac-toe game with yourself.""" + view = TicTacToe() + await inter.response.send_message(view=view) + + +# Defines a custom button that contains the logic of the game. +# The ['TicTacToe'] bit is for type hinting purposes to tell your IDE or linter +# what the type of `self.view` is. It is not required. +class TicTacToeButton(discord.ui.Button["TicTacToe"]): + + def __init__(self, x: int, y: int): + # A label is required, but we don't need one so a zero-width space is used + # The row parameter tells the View which row to place the button under. + # A View can only contain up to 5 rows -- each row can only have 5 buttons. + # Since a Tic Tac Toe grid is 3x3 that means we have 3 rows and 3 columns. + super().__init__(style=discord.ButtonStyle.secondary, label="\u200b", row=y) + self.x = x + self.y = y + + async def callback(self, interaction: discord.Interaction): + assert self.view is not None + view: TicTacToe = self.view + state = view.board[self.y][self.x] + if state in (view.X, view.O): + return + + if view.current_player == view.X: + self.style = discord.ButtonStyle.danger + self.label = "X" + self.disabled = True + view.board[self.y][self.x] = view.X + view.current_player = view.O + content = "It is now O's turn" + else: + self.style = discord.ButtonStyle.success + self.label = "O" + self.disabled = True + view.board[self.y][self.x] = view.O + view.current_player = view.X + content = "It is now X's turn" + + winner = view.check_board_winner() + if winner is not None: + if winner == view.X: + content = "X won!" + elif winner == view.O: + content = "O won!" + else: + content = "It's a tie!" + + for child in view.children: + child.disabled = True + + view.stop() + + await interaction.response.edit_message(content=content, view=view) + + +# This is our actual board View +class TicTacToe(discord.ui.View): + # This tells the IDE or linter that all our children will be TicTacToeButtons + # This is not required + children: List[TicTacToeButton] + X = -1 + O = 1 + Tie = 2 + + def __init__(self): + super().__init__() + self.current_player = self.X + self.board = [ + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + ] + + # Our board is made up of 3 by 3 TicTacToeButtons + # The TicTacToeButton maintains the callbacks and helps steer + # the actual game. + for x in range(3): + for y in range(3): + self.add_item(TicTacToeButton(x, y)) + + # This method checks for the board winner -- it is used by the TicTacToeButton + def check_board_winner(self): + for across in self.board: + value = sum(across) + if value == 3: + return self.O + elif value == -3: + return self.X + + # Check vertical + for line in range(3): + value = self.board[0][line] + self.board[1][line] + self.board[2][line] + if value == 3: + return self.O + elif value == -3: + return self.X + + # Check diagonals + diag = self.board[0][2] + self.board[1][1] + self.board[2][0] + if diag == 3: + return self.O + elif diag == -3: + return self.X + + diag = self.board[0][0] + self.board[1][1] + self.board[2][2] + if diag == 3: + return self.O + elif diag == -3: + return self.X + + # If we're here, we need to check if a tie was made + if all(i != 0 for row in self.board for i in row): + return self.Tie + + return None + + +async def setup(bot: commands.Bot): + await bot.add_cog(TicTacToeCog(bot))