diff --git a/src/exts/games/tictactoe.py b/src/exts/games/tictactoe.py new file mode 100644 index 0000000..e9cca10 --- /dev/null +++ b/src/exts/games/tictactoe.py @@ -0,0 +1,157 @@ +import games.tictactoe as ttt +import games.discord as dgames +from discord.ext import commands +import discord +from discord import app_commands + + +class View(discord.ui.View): + def __init__(self, lobby: dgames.Lobby): + super().__init__() + self.lobby = lobby + self.game = ttt.TicTacToe() + + self.knots = list(lobby.joined)[0] + self.crosses = list(lobby.joined)[1] + + self.marker_x = 0 + self.marker_y = 0 + + def get_player_for_turn(self) -> int: + if self.game.current_turn == ttt.Players.Knots: + return self.knots + else: + return self.crosses + + def show_embed(self): + if self.game.checkpoint.win_state != ttt.WinState.NoOne: + desc = "" + field = self.game.playfield.copy() + field = [[a.value for a in row] for row in field] + for i, row in enumerate(field): + desc += "".join(row) + desc += "\n" + + embed = discord.Embed( + title=self.game.checkpoint.win_state.value, + description=desc, + color=discord.Color.red(), + ) + + return embed + + desc = "" + field = self.game.playfield.copy() + field = [[a.value for a in row] for row in field] + + if self.game.current_turn == ttt.Players.Knots: + marker = "🅾️" + else: + marker = "❎" + + field[self.marker_y][self.marker_x] = marker + for i, row in enumerate(field): + desc += "".join(row) + desc += "\n" + title = f"Its {self.game.current_turn.value}'s turn" + + embed = discord.Embed(title=title, description=desc, color=discord.Color.red()) + + return embed + + async def interaction_check(self, interaction: discord.Interaction): + if interaction.user.id not in list(self.lobby.joined): + await interaction.response.send_message( + content="You are not in this game!", ephemeral=True + ) + return False + + if interaction.user.id != self.get_player_for_turn(): + await interaction.response.send_message( + content="Wait your turn!", ephemeral=True + ) + return False + + return True + + @discord.ui.button(emoji="⬅️") + async def go_left( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + self.marker_x -= 1 + + if self.marker_x == -1: + self.marker_x = self.game.size - 1 + + await interaction.response.edit_message(embed=self.show_embed(), view=self) + + @discord.ui.button(emoji="➡️") + async def go_right( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + self.marker_x += 1 + + if self.marker_x == self.game.size: + self.marker_x = 0 + + await interaction.response.edit_message(embed=self.show_embed(), view=self) + + @discord.ui.button(emoji="⬆️") + async def go_up(self, interaction: discord.Interaction, button: discord.ui.Button): + self.marker_y -= 1 + + if self.marker_y == -1: + self.marker_y = self.game.size - 1 + + await interaction.response.edit_message(embed=self.show_embed(), view=self) + + @discord.ui.button(emoji="⬇️") + async def go_down( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + self.marker_y += 1 + + if self.marker_y == self.game.size: + self.marker_y = 0 + + await interaction.response.edit_message(embed=self.show_embed(), view=self) + + @discord.ui.button(emoji="✅") + async def confirm( + self, interaction: discord.Interaction, button: discord.ui.Button + ): + try: + checkpoint = self.game.play_move(self.marker_x, self.marker_y) + if checkpoint.win_state != ttt.WinState.NoOne: + await interaction.response.edit_message( + embed=self.show_embed(), view=None + ) + return + await interaction.response.edit_message(embed=self.show_embed(), view=self) + except ValueError as e: + await interaction.response.send_message(content=str(e), ephemeral=True) + + +class Lobby(dgames.LobbyView): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.to_play = "Tic Tac Toe" + + async def on_start(self, interaction: discord.Interaction, lobby: dgames.Lobby): + view = View(lobby) + await interaction.channel.send(embed=view.show_embed(), view=view) + + +class TicTacToe(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @app_commands.command(name="tictactoe", description="Play a game of Tic Tac Toe!") + async def tictactoe(self, interaction: discord.Interaction): + # logger.debug("New tictactoe game started!") + view = Lobby(min_players=2) + await interaction.response.send_message(embed=view.make_embed(), view=view) + + +async def setup(bot: commands.Bot): + await bot.add_cog(TicTacToe(bot)) diff --git a/src/games/discord.py b/src/games/discord.py new file mode 100644 index 0000000..966c070 --- /dev/null +++ b/src/games/discord.py @@ -0,0 +1,67 @@ +import discord + + +class Lobby: + def __init__(self, min_players: int): + self.min_players: int = min_players + self.joined: set[int] = set() # Store IDs instead of objects + + def join(self, user: discord.User): + if len(self.joined) == self.min_players: + return + + self.joined.add(user.id) # Store the user ID + + def leave(self, user: discord.User): + self.joined.remove(user.id) + + +class LobbyView(discord.ui.View): + def __init__(self, min_players: int) -> None: + super().__init__() + self.lobby = Lobby(min_players=min_players) + self.to_play = "Some Unknown Game" + + def make_embed(self) -> discord.Embed: + desc = "\n".join([f"<@{user_id}>" for user_id in list(self.lobby.joined)]) + desc += f"\n\nMinimum Players: {self.lobby.min_players} | Current players: {len(self.lobby.joined)}" + return discord.Embed( + title=self.to_play, description=desc, color=discord.Color.gold() + ) + + @discord.ui.button(label="Join", emoji="👋") + async def join_lobby( + self, interaction: discord.Interaction, button: discord.ui.Button + ) -> None: + self.lobby.join(interaction.user) + await interaction.response.edit_message(embed=self.make_embed(), view=self) + + @discord.ui.button(label="Leave", emoji="🏃") + async def leave_lobby( + self, interaction: discord.Interaction, button: discord.ui.Button + ) -> None: + self.lobby.leave(interaction.user) + await interaction.response.edit_message(embed=self.make_embed(), view=self) + + @discord.ui.button(label="Start", emoji="🎮") + async def start_lobbys_game( + self, interaction: discord.Interaction, button: discord.ui.Button + ) -> None: + if len(self.lobby.joined) < self.lobby.min_players: + await interaction.response.send_message( + content="There are too little people in the lobby!", ephemeral=True + ) + return + + await interaction.message.delete() + await self.on_start(interaction, self.lobby) + + @discord.ui.button(label="Close", emoji="❌") + async def close_lobby( + self, interaction: discord.Interaction, button: discord.ui.Button + ) -> None: + await interaction.message.delete() + + # to be overriden + async def on_start(self, interaction: discord.Interaction, lobby: Lobby) -> None: + pass diff --git a/src/games/tictactoe.py b/src/games/tictactoe.py new file mode 100644 index 0000000..9c0a434 --- /dev/null +++ b/src/games/tictactoe.py @@ -0,0 +1,93 @@ +from enum import Enum +from pydantic import BaseModel + +type Playfield = list[list[Players]] + + +class Players(Enum): + Knots = "⭕" + Crosses = "❌" + Nothing = "⬛" + + +class WinState(Enum): + Knots = "⭕ won!" + Crosses = "❌ won!" + NoOne = "⬛" + Draw = "It's a draw! ❌⭕" + + +class Checkpoint(BaseModel): + win_state: WinState = WinState.NoOne + playfield: Playfield + to_play: Players + + +class TicTacToe: + def __init__(self, size: int = 3): + self.size: int = size + self.playfield: Playfield = [ + [Players.Nothing for _ in range(size)] for _ in range(size) + ] + self.current_turn: Players = Players.Knots + self.checkpoint: Checkpoint = Checkpoint( + win_state=WinState.NoOne, + playfield=self.playfield, + to_play=self.current_turn, + ) + + def switch_turns(self) -> None: + self.current_turn = ( + Players.Crosses if self.current_turn == Players.Knots else Players.Knots + ) + + def play_move(self, x: int, y: int) -> Checkpoint: + if x > (self.size - 1) or y > (self.size - 1): + raise ValueError("Position outside of available playfield") + if self.playfield[y][x] != Players.Nothing: + raise ValueError("Position already taken") + + self.playfield[y][x] = self.current_turn + self.switch_turns() + + self.checkpoint = Checkpoint( + win_state=self.check_for_win(), + playfield=self.playfield, + to_play=self.current_turn, + ) + + return self.checkpoint + + # stands for: rotated playfield clockwise + def rotated_playfield_cw(self) -> Playfield: + # rotating a matrix should NOT be this complicated!!!!! + return [list(row) for row in zip(*self.playfield[::-1])] + + def check_for_win(self) -> WinState: + for player in [Players.Knots, Players.Crosses]: + # straight horizontal + for row in self.playfield: + if [player] * self.size == row: + return WinState[player.name] + + # straight vertical + for column in self.rotated_playfield_cw(): + if [player] * self.size == column: + return WinState[player.name] + + # ltr diagonal + if all(self.playfield[i][i] == player for i in range(self.size)): + return WinState[player.name] + + # rtl diagonal + if all( + self.playfield[i][(self.size - 1) - i] == player + for i in range(self.size) + ): + return WinState[player.name] + + # Check for a draw (if no empty spaces remain) + if all(cell != Players.Nothing for row in self.playfield for cell in row): + return WinState.Draw + + return WinState.NoOne