txlyre 12 hours ago
parent
commit
28bc01ce49
5 changed files with 289 additions and 2 deletions
  1. 127 0
      chess0.py
  2. 141 0
      commands.py
  3. 2 0
      config.yml.sample
  4. 17 1
      openkriemy.py
  5. 2 1
      requirements.txt

+ 127 - 0
chess0.py

@@ -0,0 +1,127 @@
+import time
+
+import chess
+import chess.svg
+import chess.engine
+
+
+class GameOver(Exception):
+    pass
+
+
+class IllegalMove(Exception):
+    pass
+
+
+def outcome_to_str(outcome):
+    return f"{outcome.result()} {outcome.termination.name}"
+
+
+class ChessSession:
+    def __init__(self, engine):
+        self.engine = engine
+        self.board = chess.Board()
+
+        self.ts = time.time()
+        self.move_ts = time.time()
+
+    def check_game_over(self):
+        if self.board.is_game_over():
+            raise GameOver(outcome_to_str(self.board.outcome()))
+
+    async def move(self, move=None):
+        if move is not None:
+            try:
+                move = chess.Move.from_uci(move)
+            except chess.InvalidMoveError:
+                raise IllegalMove(move)
+
+            if move not in self.board.legal_moves:
+                raise IllegalMove(move)
+        else:
+            move = await self.engine.play(self.board, chess.engine.Limit(time=10))
+            move = move.move
+
+        self.board.push(move)
+        self.move_ts = time.time()
+
+        self.check_game_over()
+
+    def skip(self):
+        self.board.push(chess.Move.null())
+        self.move_ts = time.time()
+
+        self.check_game_over()
+
+    async def end(self):
+        await self.engine.quit()
+
+
+class ChessManager:
+    def __init__(self, engine_path):
+        self.engine_path = engine_path
+
+        self.sessions = {}
+
+    async def cleanup(self):
+        for id in list(self.sessions.keys()):
+            session = self.sessions[id]
+
+            if (
+                time.time() - session.move_ts >= 60 * 60 * 12
+                or time.time() - session.ts >= 60 * 60 * 24 * 7
+            ):
+                await self.end(id)
+
+    def get(self, id):
+        return self.sessions.get(id)
+
+    async def begin(self, id):
+        if id in self.sessions:
+            await self.stop(id)
+
+        _, engine = await chess.engine.popen_uci(self.engine_path)
+
+        self.sessions[id] = ChessSession(engine)
+
+    async def end(self, id):
+        session = self.sessions.get(id)
+        if not session:
+            return False
+
+        del self.sessions[id]
+
+        await session.end()
+
+        return True
+
+    async def move(self, id, move=None):
+        session = self.get(id)
+        if not session:
+            raise KeyError(id)
+
+        await session.move(move)
+
+    def undo(self, id):
+        session = self.get(id)
+        if not session:
+            raise KeyError(id)
+
+        session.board.pop()
+
+    async def skip(self, id):
+        await self.move(id)
+
+    async def svg(self, id):
+        session = self.get(id)
+        if not session:
+            raise KeyError(id)
+
+        return chess.svg.board(session.board)
+
+    async def ascii(self, id):
+        session = self.get(id)
+        if not session:
+            raise KeyError(id)
+
+        return str(session.board)

+ 141 - 0
commands.py

@@ -47,6 +47,7 @@ from utils import (
     unparse,
     unparse,
 )
 )
 from rand import roll_dices
 from rand import roll_dices
+from chess0 import IllegalMove, GameOver
 
 
 
 
 class Handler:
 class Handler:
@@ -651,6 +652,146 @@ async def roll_handler(bot, event, command):
     )
     )
 
 
 
 
+async def chess_start_handler(chess, id):
+    await chess.begin(id)
+
+    return [svg2png(chess.svg(id))]
+
+
+async def chess_stop_handler(chess, id):
+    if await chess.end(id):
+        return ["Игра завершена."]
+
+    return ["Нет активной игры."]
+
+
+async def chess_move_handler(chess, id, move):
+    try:
+        await chess.move(id, move)
+    except KeyError:
+        return ["Нет активной игры."]
+    except GameOver as e:
+        board = svg2png(chess.svg(id))
+
+        await chess.end(id)
+
+        return [
+            board,
+            f"Конец игры: {str(e)}",
+        ]
+    except IllegalMove:
+        return ["Некорректный ход."]
+
+    return [svg2png(chess.svg(id))]
+
+
+async def chess_undo_handler(chess, id):
+    try:
+        await chess.undo(id)
+    except KeyError:
+        return ["Нет активной игры."]
+    except IndexError:
+        return ["Нечего отменять."]
+
+    return ["Последний ход отменён.", svg2png(chess.svg(id))]
+
+
+async def chess_skip_handler(chess, id):
+    try:
+        await chess.skip(id)
+    except KeyError:
+        return ["Нет активной игры."]
+    except GameOver as e:
+        board = svg2png(chess.svg(id))
+
+        await chess.end(id)
+
+        return [
+            board,
+            f"Конец игры: {str(e)}",
+        ]
+
+    return [svg2png(chess.svg(id))]
+
+
+async def chess_pass_handler(chess, id):
+    try:
+        await chess.move(id)
+    except KeyError:
+        return ["Нет активной игры."]
+    except GameOver as e:
+        board = svg2png(chess.svg(id))
+
+        await chess.end(id)
+
+        return [
+            board,
+            f"Конец игры: {str(e)}",
+        ]
+
+    return [svg2png(chess.svg(id))]
+
+
+async def chess_board_handler(chess, id, move):
+    try:
+        board = svg2png(chess.svg(id))
+    except KeyError:
+        return ["Нет активной игры."]
+
+    return [board]
+
+
+CHESS_COMMANDS = {
+    "start": (chess_start_handler, "Начать новую игру", 0),
+    "end": (chess_stop_handler, "Завершить игру", 0),
+    "move": (chess_move_handler, "Сделать ход", 1),
+    "undo": (chess_undo_handler, "Отменить ход", 0),
+    "skip": (chess_skip_handler, "Пропустить ход", 0),
+    "pass": (chess_pass_handler, "Сделать ход вместо вас.", 0),
+    "board": (chess_board_handler, "Показать состояние доски"),
+}
+
+
+async def chess_handler(bot, event, command):
+    if not bot.chess:
+        await event.reply("Извините, данная функция недоступна....")
+
+        return
+
+    if command.argc < 1:
+        buffer = "Доступные субкоманды:\n"
+
+        for command in CHESS_COMMANDS:
+            buffer += f"- {command} - {CHESS_COMMANDS[command][1]}.\n"
+
+        await event.reply(buffer.strip())
+
+        return
+
+    if command.args[0] not in CHESS_COMMANDS:
+        await event.reply(f"Неизвестная субкоманда: '{command.args[0]}'.")
+
+        return
+
+    subcommand = CHESS_COMMANDS[command.args[0]]
+    if command.argc - 1 != subcommand[2]:
+        await event.reply(
+            f"Некорректное количество аргументов для субкоманды '{command.args[0]}'."
+        )
+
+        return
+
+    result = await subcommand[0](bot.chess, event.sender.id, *command.args[1:])
+
+    for reply in result:
+        if isinstance(reply, bytes):
+            await bot.send_file(
+                event.peer_id, file=reply, reply_to=event, force_document=False
+            )
+        else:
+            await event.reply(reply)
+
+
 COMMANDS = {
 COMMANDS = {
     "newadmin": Handler(newadmin_handler, is_restricted=True),
     "newadmin": Handler(newadmin_handler, is_restricted=True),
     "deladmin": Handler(deladmin_handler, is_restricted=True),
     "deladmin": Handler(deladmin_handler, is_restricted=True),

+ 2 - 0
config.yml.sample

@@ -16,3 +16,5 @@ MARKOV_CORPUS_PATH: './markov_corpus.json'
 MARKOV_TRIGGER_WORDS:
 MARKOV_TRIGGER_WORDS:
     - 'bot'
     - 'bot'
     - 'amogus'
     - 'amogus'
+
+CHESS_UCI_ENGINE: ''

+ 17 - 1
openkriemy.py

@@ -1,3 +1,4 @@
+import time
 from random import random, randint
 from random import random, randint
 from asyncio import sleep
 from asyncio import sleep
 from datetime import datetime, timedelta, date, time
 from datetime import datetime, timedelta, date, time
@@ -23,15 +24,22 @@ from actions import (
 )
 )
 from commands import COMMANDS
 from commands import COMMANDS
 from markov import Markov
 from markov import Markov
+from chess0 import ChessManager
 
 
 bot = TelegramClient("openkriemy", config.API_ID, config.API_HASH).start(
 bot = TelegramClient("openkriemy", config.API_ID, config.API_HASH).start(
     bot_token=config.API_TOKEN
     bot_token=config.API_TOKEN
 )
 )
 markov = Markov()
 markov = Markov()
 
 
+if config.CHESS_UCI_ENGINE:
+    chess = ChessManager(config.CHESS_UCI_ENGINE)
+else:
+    chess = None
+
 
 
 # Wait isn't that illegal??
 # Wait isn't that illegal??
 bot.markov = markov
 bot.markov = markov
+bot.chess = chess
 
 
 
 
 @bot.on(InlineQuery)
 @bot.on(InlineQuery)
@@ -146,7 +154,7 @@ async def on_message(event):
 
 
     try:
     try:
         await event.delete()
         await event.delete()
-    except:
+    except Exception:
         pass
         pass
 
 
     if event.sender is None:
     if event.sender is None:
@@ -208,9 +216,17 @@ async def markov_say_loop():
                 await markov_say(bot, chat.peer_id)
                 await markov_say(bot, chat.peer_id)
 
 
 
 
+async def chess_cleanup():
+    while True:
+        await sleep(60 * 60)
+        await chess.cleanup()
+
+
 with bot:
 with bot:
     bot.loop.run_until_complete(init_db())
     bot.loop.run_until_complete(init_db())
     bot.loop.create_task(notify_birthdays_loop())
     bot.loop.create_task(notify_birthdays_loop())
     bot.loop.create_task(markov_say_loop())
     bot.loop.create_task(markov_say_loop())
+    if chess:
+        bot.loop.create_task(chess_cleanup())
     bot.start()
     bot.start()
     bot.run_until_disconnected()
     bot.run_until_disconnected()

+ 2 - 1
requirements.txt

@@ -7,4 +7,5 @@ telethon
 emoji
 emoji
 markovify
 markovify
 ujson
 ujson
-spacy
+spacy
+chess