__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": ( "🧠 You want to play, let's play!\nWaiting for second" " player..." ), "gamestart_ai": "🐻 Bear is ready to compete! Are you?", "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": ( "🧠 The game is over! What a pity...\n🐉 The game ended with" " draw. No winner, no argument..." ), "normal_game": ( "🧠 {}\nPlaying with {}\n\nNow is the turn of" " {}" ), "win": ( "🧠 The game is over! What a pity...\n\n🏆 Winner: {}" " ({})\n{}" ), "ai_game": ( "🧠 {}\n{} is playing with 🐻" " Bear\n\nYou are" " {}" ), "not_with_yourself": "You can't play with yourself!", } strings_ru = { "gamestart": ( "🧠 Поиграть захотелось? Поиграем!\nОжидание второго" " игрока..." ), "gamestart_ai": "🐻 Мишка готов сражаться! А что насчет тебя?", "game_discarded": "Игра отменена", "wait_for_your_turn": "Ожидание хода", "no_move": "Эта клетка уже заполнена", "not_your_game": "Это не твоя игра, не мешай", "draw": ( "🧠 Игра окончена! Какая жалость...\n🐉 Игра закончилась" " ничьей. Нет победителя, нет спора..." ), "normal_game": ( "🧠 {}\nИгра с {}\n\nСейчас ходит {}" ), "win": ( "🧠 Игра окончена! Какая жалость...\n\n🏆 Победитель: {}" " ({})\n{}" ), "ai_game": ( "🧠 {}\n{} играет с 🐻 Мишкой\n\nТы {}" ), "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, )