feat: add the game of tic tac toe
This commit is contained in:
parent
79c3bb7427
commit
fad4f6bbbf
3 changed files with 317 additions and 0 deletions
157
src/exts/games/tictactoe.py
Normal file
157
src/exts/games/tictactoe.py
Normal file
|
|
@ -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))
|
||||
67
src/games/discord.py
Normal file
67
src/games/discord.py
Normal file
|
|
@ -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
|
||||
93
src/games/tictactoe.py
Normal file
93
src/games/tictactoe.py
Normal file
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue