201 lines
7.0 KiB
Python
201 lines
7.0 KiB
Python
# ╔══════════════════════════════════════════════════════════════════╗
|
||
# ║ ║
|
||
# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║
|
||
# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║
|
||
# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║
|
||
# ║ ║
|
||
# ║ © 2026 CodeX Devs — All Rights Reserved ║
|
||
# ║ ║
|
||
# ║ discord ── https://discord.gg/codexdev ║
|
||
# ║ youtube ── https://youtube.com/@CodeXDevs ║
|
||
# ║ github ── https://github.com/RayExo ║
|
||
# ║ ║
|
||
# ╚══════════════════════════════════════════════════════════════════╝
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Optional, ClassVar
|
||
import asyncio
|
||
|
||
import discord
|
||
from discord.ext import commands
|
||
from utils.emoji import ERROR
|
||
|
||
from .utils import DiscordColor, DEFAULT_COLOR
|
||
|
||
|
||
class Tictactoe:
|
||
"""
|
||
TicTacToe Game
|
||
"""
|
||
|
||
BLANK: ClassVar[str] = "⬛"
|
||
CIRCLE: ClassVar[str] = "⭕"
|
||
CROSS: ClassVar[str] = ERROR
|
||
_conversion: ClassVar[dict[str, tuple[int, int]]] = {
|
||
"1️⃣": (0, 0),
|
||
"2️⃣": (0, 1),
|
||
"3️⃣": (0, 2),
|
||
"4️⃣": (1, 0),
|
||
"5️⃣": (1, 1),
|
||
"6️⃣": (1, 2),
|
||
"7️⃣": (2, 0),
|
||
"8️⃣": (2, 1),
|
||
"9️⃣": (2, 2),
|
||
}
|
||
|
||
_WINNERS: ClassVar[tuple[tuple[tuple[int, int], ...], ...]] = (
|
||
((0, 0), (0, 1), (0, 2)),
|
||
((1, 0), (1, 1), (1, 2)),
|
||
((2, 0), (2, 1), (2, 2)),
|
||
((0, 0), (1, 0), (2, 0)),
|
||
((0, 1), (1, 1), (2, 1)),
|
||
((0, 2), (1, 2), (2, 2)),
|
||
((0, 0), (1, 1), (2, 2)),
|
||
((0, 2), (1, 1), (2, 0)),
|
||
)
|
||
|
||
def __init__(self, cross: discord.User, circle: discord.User) -> None:
|
||
self.cross = cross
|
||
self.circle = circle
|
||
|
||
self.board: list[list[str]] = [[self.BLANK for _ in range(3)] for _ in range(3)]
|
||
self.turn: discord.User = self.cross
|
||
|
||
self.winner: Optional[discord.User] = None
|
||
self.winning_indexes: list[tuple[int, int]] = []
|
||
self.message: Optional[discord.Message] = None
|
||
|
||
self._controls: list[str] = [
|
||
"1️⃣",
|
||
"2️⃣",
|
||
"3️⃣",
|
||
"4️⃣",
|
||
"5️⃣",
|
||
"6️⃣",
|
||
"7️⃣",
|
||
"8️⃣",
|
||
"9️⃣",
|
||
]
|
||
|
||
self.emoji_to_player: dict[discord.User, str] = {
|
||
self.CIRCLE: self.circle,
|
||
self.CROSS: self.cross,
|
||
}
|
||
self.player_to_emoji: dict[str, discord.User] = {
|
||
v: k for k, v in self.emoji_to_player.items()
|
||
}
|
||
|
||
def board_string(self) -> str:
|
||
board = ""
|
||
for row in self.board:
|
||
board += "".join(row) + "\n"
|
||
return board
|
||
|
||
def make_embed(self, *, game_over: bool = False) -> discord.Embed:
|
||
embed = discord.Embed(color=discord.Color.random())
|
||
if game_over:
|
||
status = f"{self.winner.mention} won!" if self.winner else "Tie"
|
||
embed.description = f"**Game over**\n{status}"
|
||
else:
|
||
embed.description = f"**Turn:** {self.turn.mention}\n**Piece:** `{self.player_to_emoji[self.turn]}`"
|
||
return embed
|
||
|
||
def make_move(self, emoji: str, user: discord.User) -> list:
|
||
|
||
if emoji not in self._controls:
|
||
raise KeyError("Provided emoji is not one of the valid controls")
|
||
else:
|
||
x, y = self._conversion[emoji]
|
||
piece = self.player_to_emoji[user]
|
||
self.board[x][y] = piece
|
||
|
||
self.turn = self.circle if user == self.cross else self.cross
|
||
self._conversion.pop(emoji)
|
||
self._controls.remove(emoji)
|
||
return self.board
|
||
|
||
def is_game_over(self, *, tie: bool = False) -> bool:
|
||
|
||
for possibility in self._WINNERS:
|
||
row = [self.board[r][c] for r, c in possibility]
|
||
|
||
if len(set(row)) == 1 and row[0] != self.BLANK:
|
||
self.winner = self.emoji_to_player[row[0]]
|
||
self.winning_indexes = possibility
|
||
return True
|
||
|
||
if not self._controls or tie:
|
||
return True
|
||
|
||
return False
|
||
|
||
async def start(
|
||
self,
|
||
ctx: commands.Context[commands.Bot],
|
||
*,
|
||
timeout: Optional[float] = None,
|
||
embed_color: DiscordColor = discord.Color.random(),
|
||
remove_reaction_after: bool = False,
|
||
**kwargs,
|
||
) -> discord.Message:
|
||
"""
|
||
starts the tictactoe game
|
||
|
||
Parameters
|
||
----------
|
||
ctx : commands.Context
|
||
the context of the invokation command
|
||
timeout : Optional[float], optional
|
||
the timeout for when waiting, by default None
|
||
embed_color : DiscordColor, optional
|
||
the color of the game embed, by default DEFAULT_COLOR
|
||
remove_reaction_after : bool, optional
|
||
specifies whether or not to remove the move reaction each time, by default False
|
||
|
||
Returns
|
||
-------
|
||
discord.Message
|
||
returns the game emssage
|
||
"""
|
||
self.embed_color = embed_color
|
||
|
||
embed = self.make_embed()
|
||
self.message = await ctx.send(self.board_string(), embed=embed, **kwargs)
|
||
|
||
for button in self._controls:
|
||
await self.message.add_reaction(button)
|
||
|
||
while not ctx.bot.is_closed():
|
||
|
||
def check(reaction: discord.Reaction, user: discord.User) -> bool:
|
||
return (
|
||
str(reaction.emoji) in self._controls
|
||
and user == self.turn
|
||
and reaction.message == self.message
|
||
)
|
||
|
||
try:
|
||
reaction, user = await ctx.bot.wait_for(
|
||
"reaction_add", timeout=timeout, check=check
|
||
)
|
||
except asyncio.TimeoutError:
|
||
break
|
||
|
||
if self.is_game_over():
|
||
break
|
||
|
||
emoji = str(reaction.emoji)
|
||
self.make_move(emoji, user)
|
||
embed = self.make_embed()
|
||
|
||
if remove_reaction_after:
|
||
await self.message.remove_reaction(emoji, user)
|
||
|
||
await self.message.edit(content=self.board_string(), embed=embed)
|
||
|
||
embed = self.make_embed(game_over=True)
|
||
await self.message.edit(content=self.board_string(), embed=embed)
|
||
|
||
return self.message
|