mirror of
https://github.com/MuRuLOSE/limoka.git
synced 2026-06-16 14:34:17 +02:00
571 lines
18 KiB
Python
Executable File
571 lines
18 KiB
Python
Executable File
__version__ = (2, 0, 0)
|
||
|
||
# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀
|
||
# █▀█ █ █ █ █▀█ █▀▄ █
|
||
# © Copyright 2022
|
||
# https://t.me/hikariatama
|
||
#
|
||
# 🔒 Licensed under the GNU AGPLv3
|
||
# 🌐 https://www.gnu.org/licenses/agpl-3.0.html
|
||
|
||
# meta pic: https://static.dan.tatar/tictactoe_icon.png
|
||
# meta banner: https://mods.hikariatama.ru/badges/tictactoe.jpg
|
||
# meta developer: @hikarimods
|
||
# scope: inline
|
||
# scope: hikka_only
|
||
# scope: hikka_min 1.2.10
|
||
|
||
import copy
|
||
import enum
|
||
from random import choice
|
||
from typing import List
|
||
|
||
from telethon.tl.types import Message
|
||
from telethon.utils import get_display_name
|
||
|
||
from .. import loader, utils
|
||
from ..inline.types import InlineCall
|
||
|
||
phrases = [
|
||
"Your brain is just a joke... Use it!",
|
||
"What a nice move...",
|
||
"Try to overcome me!",
|
||
"I'm irresistible, you have no chances!",
|
||
"The clock is ticking... Hurry up.",
|
||
"Don't act, stop to think!",
|
||
"It was your choice, not mine...",
|
||
]
|
||
|
||
# AI from https://github.com/morgankenyon/RandomML/tree/master/src/tictactoe
|
||
|
||
|
||
class Player(enum.Enum):
|
||
x = 1
|
||
o = 2
|
||
|
||
@property
|
||
def other(self):
|
||
return Player.x if self == Player.o else Player.o
|
||
|
||
|
||
class Choice:
|
||
def __init__(self, move, value, depth):
|
||
self.move = move
|
||
self.value = value
|
||
self.depth = depth
|
||
|
||
def __str__(self):
|
||
return f"{str(self.move)}: {str(self.value)}"
|
||
|
||
|
||
class AbBot:
|
||
def __init__(self, player):
|
||
self.player = player
|
||
|
||
def alpha_beta_search(self, board, is_max, current_player, depth, alpha, beta):
|
||
# if board has a winner or is a tie
|
||
# return with appropriate values
|
||
winner = board.has_winner()
|
||
if winner == self.player:
|
||
return Choice(board.last_move(), 10 - depth, depth)
|
||
elif winner == self.player.other:
|
||
return Choice(board.last_move(), -10 + depth, depth)
|
||
elif len(board.moves) == 9:
|
||
return Choice(board.last_move(), 0, depth)
|
||
|
||
candidates = board.get_legal_moves()
|
||
max_choice = None
|
||
min_choice = None
|
||
for i in range(len(candidates)):
|
||
row = candidates[i][0]
|
||
col = candidates[i][1]
|
||
newboard = copy.deepcopy(board)
|
||
newboard.make_move(row, col, current_player)
|
||
result = self.alpha_beta_search(
|
||
newboard, not is_max, current_player.other, depth + 1, alpha, beta
|
||
)
|
||
result.move = newboard.last_move()
|
||
|
||
if is_max:
|
||
alpha = max(result.value, alpha)
|
||
if alpha >= beta:
|
||
return result
|
||
|
||
if max_choice is None or result.value > max_choice.value:
|
||
max_choice = result
|
||
else:
|
||
beta = min(result.value, beta)
|
||
if alpha >= beta:
|
||
return result
|
||
|
||
if min_choice is None or result.value < min_choice.value:
|
||
min_choice = result
|
||
|
||
return max_choice if is_max else min_choice
|
||
|
||
def select_move(self, board):
|
||
choice = self.alpha_beta_search(board, True, self.player, 0, -100, 100)
|
||
return choice.move
|
||
|
||
|
||
MARKER_TO_CHAR = {
|
||
None: " . ",
|
||
Player.x: " x ",
|
||
Player.o: " o ",
|
||
}
|
||
|
||
|
||
class Board:
|
||
def __init__(self):
|
||
self.dimension = 3
|
||
self.grid = [
|
||
[None for _ in range(self.dimension)] for _ in range(self.dimension)
|
||
]
|
||
|
||
self.moves = []
|
||
|
||
def print(self):
|
||
print()
|
||
for row in range(self.dimension):
|
||
line = [
|
||
MARKER_TO_CHAR[self.grid[row][col]] for col in range(self.dimension)
|
||
]
|
||
print("%s" % "".join(line))
|
||
|
||
def has_winner(self):
|
||
# need at least 5 moves before x hits three in a row
|
||
if len(self.moves) < 5:
|
||
return None
|
||
|
||
# check rows for win
|
||
for row in range(self.dimension):
|
||
unique_rows = set(self.grid[row])
|
||
if len(unique_rows) == 1:
|
||
value = unique_rows.pop()
|
||
if value is not None:
|
||
return value
|
||
|
||
# check columns for win
|
||
for col in range(self.dimension):
|
||
unique_cols = {self.grid[row][col] for row in range(self.dimension)}
|
||
if len(unique_cols) == 1:
|
||
value = unique_cols.pop()
|
||
if value is not None:
|
||
return value
|
||
|
||
# check backwards diagonal (top left to bottom right) for win
|
||
backwards_diag = {self.grid[0][0], self.grid[1][1], self.grid[2][2]}
|
||
if len(backwards_diag) == 1:
|
||
value = backwards_diag.pop()
|
||
if value is not None:
|
||
return value
|
||
|
||
# check forwards diagonal (bottom left to top right) for win
|
||
forwards_diag = {self.grid[2][0], self.grid[1][1], self.grid[0][2]}
|
||
if len(forwards_diag) == 1:
|
||
value = forwards_diag.pop()
|
||
if value is not None:
|
||
return value
|
||
|
||
# found no winner, return None
|
||
return None
|
||
|
||
def make_move(self, row, col, player):
|
||
if self.is_space_empty(row, col):
|
||
self.grid[row][col] = player
|
||
self.moves.append([row, col])
|
||
else:
|
||
raise Exception("Attempting to move onto already occupied space")
|
||
|
||
def last_move(self):
|
||
return self.moves[-1]
|
||
|
||
def is_space_empty(self, row, col):
|
||
return self.grid[row][col] is None
|
||
|
||
def get_legal_moves(self):
|
||
choices = []
|
||
for row in range(self.dimension):
|
||
choices.extend(
|
||
[row, col]
|
||
for col in range(self.dimension)
|
||
if (self.is_space_empty(row, col))
|
||
)
|
||
|
||
return choices
|
||
|
||
def __deepcopy__(self, memodict=None):
|
||
if memodict is None:
|
||
memodict = {}
|
||
dp = Board()
|
||
dp.grid = copy.deepcopy(self.grid)
|
||
dp.moves = copy.deepcopy(self.moves)
|
||
return dp
|
||
|
||
|
||
# /AI
|
||
|
||
|
||
@loader.tds
|
||
class TicTacToeMod(loader.Module):
|
||
"""Play your favorite game in Telegram"""
|
||
|
||
strings = {
|
||
"name": "TicTacToe",
|
||
"gamestart": (
|
||
"🧠 <b>You want to play, let's play!</b>\n<i>Waiting for second"
|
||
" player...</i>"
|
||
),
|
||
"gamestart_ai": "🐻 <b>Bear is ready to compete! Are you?</b>",
|
||
"game_discarded": "Game is discarded",
|
||
"wait_for_your_turn": "Wait for your turn",
|
||
"no_move": "This cell is not empty",
|
||
"not_your_game": "It is not your game, don't interrupt it",
|
||
"draw": (
|
||
"🧠 <b>The game is over! What a pity...</b>\n<i>🐉 The game ended with"
|
||
" <b>draw</b>. No winner, no argument...</i>"
|
||
),
|
||
"normal_game": (
|
||
"🧠 <b>{}</b>\n<i>Playing with <b>{}</b></i>\n\n<i>Now is the turn of"
|
||
" <b>{}</b></i>"
|
||
),
|
||
"win": (
|
||
"🧠 <b>The game is over! What a pity...</b>\n\n<i>🏆 Winner: <b>{}"
|
||
" ({})</b></i>\n<code>{}</code>"
|
||
),
|
||
"ai_game": (
|
||
"🧠 <b>{}</b>\n<i><b>{}</b> is playing with <b>🐻"
|
||
" Bear</b></i>\n\n<i>You are"
|
||
" {}</i>"
|
||
),
|
||
"not_with_yourself": "You can't play with yourself!",
|
||
}
|
||
|
||
strings_ru = {
|
||
"gamestart": (
|
||
"🧠 <b>Поиграть захотелось? Поиграем!</b>\n<i>Ожидание второго"
|
||
" игрока...</i>"
|
||
),
|
||
"gamestart_ai": "🐻 <b>Мишка готов сражаться! А что насчет тебя?</b>",
|
||
"game_discarded": "Игра отменена",
|
||
"wait_for_your_turn": "Ожидание хода",
|
||
"no_move": "Эта клетка уже заполнена",
|
||
"not_your_game": "Это не твоя игра, не мешай",
|
||
"draw": (
|
||
"🧠 <b>Игра окончена! Какая жалость...</b>\n<i>🐉 Игра закончилась"
|
||
" <b>ничьей</b>. Нет победителя, нет спора...</i>"
|
||
),
|
||
"normal_game": (
|
||
"🧠 <b>{}</b>\n<i>Игра с <b>{}</b></i>\n\n<i>Сейчас ходит <b>{}</b></i>"
|
||
),
|
||
"win": (
|
||
"🧠 <b>Игра окончена! Какая жалость...</b>\n\n<i>🏆 Победитель: <b>{}"
|
||
" ({})</b></i>\n<code>{}</code>"
|
||
),
|
||
"ai_game": (
|
||
"🧠 <b>{}</b>\n<i><b>{}</b> играет с <b>🐻 Мишкой</b></i>\n\n<i>Ты {}</i>"
|
||
),
|
||
"not_with_yourself": "Ты не можешь играть сам с собой!",
|
||
"_cmd_doc_tictactoe": "Начать новую игру в крестики-нолики",
|
||
"_cmd_doc_tictacai": "Сыграть с 🐻 Мишкой (У тебя нет шансов)",
|
||
"_cls_doc": "Сыграй в крестики-нолики прямо в Телеграм",
|
||
}
|
||
|
||
async def client_ready(self, client, db):
|
||
self._games = {}
|
||
self._me = await client.get_me()
|
||
|
||
async def _process_click(
|
||
self,
|
||
call: InlineCall,
|
||
i: int,
|
||
j: int,
|
||
line: str,
|
||
):
|
||
if call.from_user.id not in [
|
||
self._me.id,
|
||
self._games[call.form["uid"]]["2_player"],
|
||
]:
|
||
await call.answer(self.strings("not_your_game"))
|
||
return
|
||
|
||
if call.from_user.id != self._games[call.form["uid"]]["turn"]:
|
||
await call.answer(self.strings("wait_for_your_turn"))
|
||
return
|
||
|
||
if line != ".":
|
||
await call.answer(self.strings("no_move"))
|
||
return
|
||
|
||
self._games[call.form["uid"]]["score"] = (
|
||
self._games[call.form["uid"]]["score"][: j + i * 4]
|
||
+ self._games[call.form["uid"]]["mapping"][call.from_user.id]
|
||
+ self._games[call.form["uid"]]["score"][j + i * 4 + 1 :]
|
||
)
|
||
|
||
self._games[call.form["uid"]]["turn"] = (
|
||
self._me.id
|
||
if call.from_user.id != self._me.id
|
||
else self._games[call.form["uid"]]["2_player"]
|
||
)
|
||
|
||
await call.edit(**self._render(call.form["uid"]))
|
||
|
||
async def _process_click_ai(self, call: InlineCall, i: int, j: int, line: str):
|
||
if call.form["uid"] not in self._games:
|
||
await call.answer(self.strings("game_discarded"))
|
||
await call.delete()
|
||
|
||
if call.from_user.id != self._games[call.form["uid"]]["user"].id:
|
||
await call.answer(self.strings("not_your_game"))
|
||
return
|
||
|
||
if line != ".":
|
||
await call.answer(self.strings("no_move"))
|
||
return
|
||
|
||
self._games[call.form["uid"]]["board"].make_move(
|
||
i, j, self._games[call.form["uid"]]["human_player"]
|
||
)
|
||
|
||
try:
|
||
self._games[call.form["uid"]]["board"].make_move(
|
||
*self._games[call.form["uid"]]["bot"].select_move(
|
||
self._games[call.form["uid"]]["board"]
|
||
),
|
||
self._games[call.form["uid"]]["ai_player"],
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
await call.edit(**self._render_ai(call.form["uid"]))
|
||
|
||
def win_indexes(self, n):
|
||
return (
|
||
[[(r, c) for r in range(n)] for c in range(n)]
|
||
+ [[(r, c) for c in range(n)] for r in range(n)]
|
||
+ [[(i, i) for i in range(n)]]
|
||
+ [[(i, n - 1 - i) for i in range(n)]]
|
||
)
|
||
|
||
def is_winner(self, board, decorator):
|
||
n = len(board)
|
||
|
||
return any(
|
||
all(board[r][c] == decorator for r, c in indexes)
|
||
for indexes in self.win_indexes(n)
|
||
)
|
||
|
||
def _render_text(self, board_raw: List[List[str]]) -> str:
|
||
board = [[char.replace(".", " ") for char in line] for line in board_raw]
|
||
return f"""
|
||
{board[0][0]} | {board[0][1]} | {board[0][2]}
|
||
----------
|
||
{board[1][0]} | {board[1][1]} | {board[1][2]}
|
||
----------
|
||
{board[2][0]} | {board[2][1]} | {board[2][2]}"""
|
||
|
||
def _render(self, uid: str) -> dict:
|
||
if uid not in self._games or uid not in self.inline._units:
|
||
return
|
||
|
||
game = self._games[uid]
|
||
text = self.strings("normal_game").format(
|
||
choice(phrases),
|
||
game["name"],
|
||
(
|
||
utils.escape_html(get_display_name(self._me))
|
||
if game["turn"] == self._me.id
|
||
else game["name"]
|
||
),
|
||
)
|
||
score = game["score"].split("|")
|
||
kb = []
|
||
rmap = {v: k for k, v in game["mapping"].items()}
|
||
|
||
win_x, win_o = self.is_winner(score, "x"), self.is_winner(score, "o")
|
||
|
||
if win_o or win_x:
|
||
try:
|
||
del self._games[uid]
|
||
except KeyError:
|
||
pass
|
||
|
||
winner = rmap["x" if win_x else "o"]
|
||
|
||
return {
|
||
"text": self.strings("win").format(
|
||
(
|
||
game["name"]
|
||
if winner != self._me.id
|
||
else utils.escape_html(get_display_name(self._me))
|
||
),
|
||
"❌" if win_x else "⭕️",
|
||
self._render_text(score),
|
||
)
|
||
}
|
||
|
||
if game["score"].count("."):
|
||
for i, row in enumerate(score):
|
||
kb_row = [
|
||
{
|
||
"text": (
|
||
line.replace(".", " ").replace("x", "❌").replace("o", "⭕️")
|
||
),
|
||
"callback": self._process_click,
|
||
"args": (
|
||
i,
|
||
j,
|
||
line,
|
||
),
|
||
}
|
||
for j, line in enumerate(row)
|
||
]
|
||
kb += [kb_row]
|
||
else:
|
||
try:
|
||
del self._games[uid]
|
||
except KeyError:
|
||
pass
|
||
|
||
return {"text": self.strings("draw")}
|
||
|
||
return {"text": text, "reply_markup": kb}
|
||
|
||
async def inline__start_game(self, call: InlineCall):
|
||
if call.from_user.id == self._me.id:
|
||
await call.answer(self.strings("not_with_yourself"))
|
||
return
|
||
|
||
uid = call.form["uid"]
|
||
first = choice([call.from_user.id, self._me.id])
|
||
self._games[uid] = {
|
||
"2_player": call.from_user.id,
|
||
"turn": first,
|
||
"mapping": {
|
||
first: "x",
|
||
(call.from_user.id if call.from_user.id != first else self._me.id): "o",
|
||
},
|
||
"name": utils.escape_html(
|
||
get_display_name(await self._client.get_entity(call.from_user.id))
|
||
),
|
||
"score": "...|...|...",
|
||
}
|
||
|
||
await call.edit(**self._render(uid))
|
||
|
||
async def inline__start_game_ai(self, call: InlineCall):
|
||
uid = call.form["uid"]
|
||
|
||
user = await self._client.get_entity(call.from_user.id)
|
||
|
||
first = choice(["bear", user.id])
|
||
self._games[uid] = {
|
||
"2_player": "bear",
|
||
"turn": user.id,
|
||
"mapping": {first: "x", "bear" if first != "bear" else user.id: "o"},
|
||
"amifirst": first == user.id,
|
||
"user": user,
|
||
"ai_player": Player.x if first == "bear" else Player.o,
|
||
"human_player": Player.o if first == "bear" else Player.x,
|
||
"bot": AbBot(Player.x if first == "bear" else Player.o),
|
||
"board": Board(),
|
||
}
|
||
|
||
if first == "bear":
|
||
self._games[uid]["board"].make_move(
|
||
*self._games[uid]["bot"].select_move(self._games[uid]["board"]),
|
||
self._games[uid]["ai_player"],
|
||
)
|
||
|
||
await call.edit(**self._render_ai(uid))
|
||
|
||
async def tictactoecmd(self, message: Message):
|
||
"""Start new tictactoe game"""
|
||
await self.inline.form(
|
||
self.strings("gamestart"),
|
||
message=message,
|
||
reply_markup={"text": "💪 Play", "callback": self.inline__start_game},
|
||
ttl=15 * 60,
|
||
disable_security=True,
|
||
)
|
||
|
||
def _render_ai(self, uid: str) -> dict:
|
||
if uid not in self._games or uid not in self.inline._units:
|
||
return
|
||
|
||
game = self._games[uid]
|
||
text = self.strings("ai_game").format(
|
||
choice(phrases),
|
||
utils.escape_html(get_display_name(game["user"])),
|
||
"❌" if game["amifirst"] else "⭕️",
|
||
)
|
||
score = [
|
||
[MARKER_TO_CHAR[char].strip() for char in line]
|
||
for line in game["board"].grid
|
||
]
|
||
kb = []
|
||
rmap = {v: k for k, v in game["mapping"].items()}
|
||
|
||
win_x, win_o = self.is_winner(score, "x"), self.is_winner(score, "o")
|
||
|
||
if win_o or win_x:
|
||
try:
|
||
del self._games[uid]
|
||
except KeyError:
|
||
pass
|
||
|
||
winner = rmap["x" if win_x else "o"]
|
||
|
||
return {
|
||
"text": self.strings("win").format(
|
||
(
|
||
"🐻 Bear"
|
||
if winner != game["user"]
|
||
else utils.escape_html(get_display_name(game["user"]))
|
||
),
|
||
"❌" if win_x else "⭕️",
|
||
self._render_text(score),
|
||
)
|
||
}
|
||
|
||
if "".join(["".join(line) for line in score]).count("."):
|
||
for i, row in enumerate(score):
|
||
kb_row = [
|
||
{
|
||
"text": (
|
||
line.replace(".", " ").replace("x", "❌").replace("o", "⭕️")
|
||
),
|
||
"callback": self._process_click_ai,
|
||
"args": (
|
||
i,
|
||
j,
|
||
line,
|
||
),
|
||
}
|
||
for j, line in enumerate(row)
|
||
]
|
||
kb += [kb_row]
|
||
else:
|
||
try:
|
||
del self._games[uid]
|
||
except KeyError:
|
||
pass
|
||
|
||
return {"text": self.strings("draw")}
|
||
|
||
return {"text": text, "reply_markup": kb}
|
||
|
||
async def tictacaicmd(self, message: Message):
|
||
"""Play with 🐻 Bear (You have no chances to win)"""
|
||
await self.inline.form(
|
||
self.strings("gamestart_ai"),
|
||
message=message,
|
||
reply_markup={
|
||
"text": "🧠 Let's go!",
|
||
"callback": self.inline__start_game_ai,
|
||
},
|
||
ttl=15 * 60,
|
||
disable_security=True,
|
||
)
|