Ruby-Cogs/ttt/ttt.py
2025-02-19 22:02:13 -05:00

256 lines
7.5 KiB
Python

# Ported from https://github.com/hizkifw/discord-tictactoe
# This cog is licensed under Apache-2.0, which is bundled with the cog file under LICENSE.
import discord
import logging
from redbot.core import commands
log = logging.getLogger("red.aikaterna.ttt")
class TTT(commands.Cog):
"""
Tic Tac Toe
"""
def __init__(self, bot):
self.bot = bot
self.ttt_games = {}
async def red_delete_data_for_user(self, **kwargs):
"""Nothing to delete"""
return
@commands.guild_only()
@commands.bot_has_permissions(add_reactions=True)
@commands.max_concurrency(1, commands.BucketType.user)
@commands.command()
async def ttt(self, ctx, move=""):
""" Tic Tac Toe """
await self.ttt_new(ctx.author, ctx.channel)
async def ttt_new(self, user, channel):
self.ttt_games[user.id] = [" "] * 9
response = self._make_board(user)
response += "Your move:"
msg = await channel.send(response)
await self._make_buttons(msg)
async def ttt_move(self, user, message, move):
log.debug(f"ttt_move:{user.id}")
# Check user currently playing
if user.id not in self.ttt_games:
log.debug("New ttt game")
return await self.ttt_new(user, message.channel)
# Check spot is empty
if self.ttt_games[user.id][move] == " ":
self.ttt_games[user.id][move] = "x"
log.debug(f"Moved to {move}")
else:
log.debug(f"Invalid move: {move}")
return None
# Check winner
check = self._do_checks(self.ttt_games[user.id])
if check is not None:
msg = "It's a draw!" if check == "draw" else f"{check[-1]} wins!"
log.debug(msg)
await message.edit(content=f"{self._make_board(user)}{msg}")
return None
log.debug("Check passed")
# AI move
mv = self._ai_think(self._matrix(self.ttt_games[user.id]))
self.ttt_games[user.id][self._coords_to_index(mv)] = "o"
log.debug("AI moved")
# Update board
await message.edit(content=self._make_board(user))
log.debug("Board updated")
# Check winner again
check = self._do_checks(self.ttt_games[user.id])
if check is not None:
msg = "It's a draw!" if check == "draw" else f"{check[-1]} wins!"
log.debug(msg)
await message.edit(content=f"{self._make_board(user)}{msg}")
log.debug("Check passed")
def _make_board(self, author):
return f"{author.mention}\n{self._table(self.ttt_games[author.id])}\n"
async def _make_buttons(self, msg):
await msg.add_reaction("\u2196") # 0 tl
await msg.add_reaction("\u2B06") # 1 t
await msg.add_reaction("\u2197") # 2 tr
await msg.add_reaction("\u2B05") # 3 l
await msg.add_reaction("\u23FA") # 4 mid
await msg.add_reaction("\u27A1") # 5 r
await msg.add_reaction("\u2199") # 6 bl
await msg.add_reaction("\u2B07") # 7 b
await msg.add_reaction("\u2198") # 8 br
@commands.Cog.listener()
async def on_reaction_add(self, reaction, user):
if reaction.message.guild is None:
return
if reaction.message.author != self.bot.user:
return
game_session = self.ttt_games.get(user.id, None)
if game_session is None:
return
move = self._decode_move(str(reaction.emoji))
if move is None:
return
await self.ttt_move(user, reaction.message, move)
@staticmethod
def _decode_move(emoji):
dict = {
"\u2196": 0,
"\u2B06": 1,
"\u2197": 2,
"\u2B05": 3,
"\u23FA": 4,
"\u27A1": 5,
"\u2199": 6,
"\u2B07": 7,
"\u2198": 8,
}
return dict[emoji] if emoji in dict else None
@staticmethod
def _table(xo):
return (
(("%s%s%s\n" * 3) % tuple(xo))
.replace("o", ":o2:")
.replace("x", ":regional_indicator_x:")
.replace(" ", ":white_large_square:")
)
@staticmethod
def _matrix(b):
return [[b[0], b[1], b[2]], [b[3], b[4], b[5]], [b[6], b[7], b[8]]]
@staticmethod
def _coords_to_index(coords):
map = {(0, 0): 0, (0, 1): 1, (0, 2): 2, (1, 0): 3, (1, 1): 4, (1, 2): 5, (2, 0): 6, (2, 1): 7, (2, 2): 8}
return map[coords]
def _do_checks(self, b):
m = self._matrix(b)
if self._check_win(m, "x"):
return "win X"
if self._check_win(m, "o"):
return "win O"
if self._check_draw(b):
return "draw"
return None
# The following comes from an old project
# https://gist.github.com/HizkiFW/0aadefb73e71794fb4a2802708db5bcf
@staticmethod
def _find_streaks(m, xo):
row = [0, 0, 0]
col = [0, 0, 0]
dia = [0, 0]
# Check rows and columns for X streaks
for y in range(3):
for x in range(3):
if m[y][x] == xo:
row[y] += 1
col[x] += 1
# Check diagonals
if m[0][0] == xo:
dia[0] += 1
if m[1][1] == xo:
dia[0] += 1
dia[1] += 1
if m[2][2] == xo:
dia[0] += 1
if m[2][0] == xo:
dia[1] += 1
if m[0][2] == xo:
dia[1] += 1
return (row, col, dia)
@staticmethod
def _find_empty(matrix, rcd, n):
# Rows
if rcd == "r":
for x in range(3):
if matrix[n][x] == " ":
return x
# Columns
if rcd == "c":
for x in range(3):
if matrix[x][n] == " ":
return x
# Diagonals
if rcd == "d":
if n == 0:
for x in range(3):
if matrix[x][x] == " ":
return x
else:
for x in range(3):
if matrix[x][2 - x] == " ":
return x
return False
def _check_win(self, m, xo):
row, col, dia = self._find_streaks(m, xo)
dia.append(0)
for i in range(3):
if row[i] == 3 or col[i] == 3 or dia[i] == 3:
return True
return False
@staticmethod
def _check_draw(board):
return not " " in board
def _ai_think(self, m):
rx, cx, dx = self._find_streaks(m, "x")
ro, co, do = self._find_streaks(m, "o")
mv = self._ai_move(2, m, ro, co, do)
if mv is not False:
return mv
mv = self._ai_move(2, m, rx, cx, dx)
if mv is not False:
return mv
mv = self._ai_move(1, m, ro, co, do)
if mv is not False:
return mv
return self._ai_move(1, m, rx, cx, dx)
def _ai_move(self, n, m, row, col, dia):
for r in range(3):
if row[r] == n:
x = self._find_empty(m, "r", r)
if x is not False:
return (r, x)
if col[r] == n:
y = self._find_empty(m, "c", r)
if y is not False:
return (y, r)
if dia[0] == n:
y = self._find_empty(m, "d", 0)
if y is not False:
return (y, y)
if dia[1] == n:
y = self._find_empty(m, "d", 1)
if y is not False:
return (y, 2 - y)
return False