Files
Mace-AIO-Discord-Bot---With…/bot/games/tictactoe.py
2026-07-16 11:14:48 +05:30

201 lines
7.0 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ╔══════════════════════════════════════════════════════════════════╗
# ║ ║
# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║
# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║
# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║
# ║ ║
# ║ © 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