Telegram-бот на Python и aiogram: играем в шахматы

В предыдущей части мы написали базового бота, который отвечал нам текстом нашего же сообщения, однако это не очень полезно.

Давайте теперь сделаем так, чтобы бот мог играть с нами в шахматы.

Дополнительные библиотеки

Установим python-chess и cairosvg

pip install python-chess cairosvg

Куда добавлять код?

Мы будем писать обработчики сообщений между обработчиком команды /start и обработчиком всех остальных сообщений в коде из статьи предыдущей части.

Также мы добавим новый модуль (назовем его chess_utils), где мы будем писать функции, не относящиеся напрямую к телеграм-боту, а к логике, связанной с шахматами.

Команда /newgame

Напишем команду /newgame, которая будет начинать новую партию с ботом в шахматы.

@dp.message(Command("newgame"))
async def start_game(message: Message) -> None:
    initial_board = chess.Board()
    boards[message.from_user.id] = initial_board
    board_image = generate_board_image(initial_board)
    await message.answer_photo(
        photo=BufferedInputFile(board_image, "game.png"),
        caption="Игра началась! Делай первый ход. Например: e2e4"
    )

После ввода этой команды мы отправляем фото текущего состояния доски (которое есть начальное состояние), и приглашаем пользователя сделать ход.

Мы используем экземпляр chess.Board из пакета python-chess для управления шахматной доской, и вспомогательную функцию generate_board_image для того, чтобы из внутреннего представления доски сделать изображение, которое можно отправить пользователю в Telegram:

Файл chess_utils.py (поместить в ту же директорию, что и bot.py)

from io import BytesIO
import random

import chess
import chess.svg
from cairosvg import svg2png


def generate_board_image(board: chess.Board) -> bytes:
    svg_image = chess.svg.board(board)
    png_image = BytesIO()
    svg2png(bytestring=svg_image, write_to=png_image)
    png_image.seek(0)
    return png_image.read()

А теперь начнем обрабатывать ходы пользователя. Для этого перепишем функцию echo из предыдущей части. Напомню, что ранее она просто отвечала текстом написанного сообщения.

Теперь же вместо функции echo будет функция process_move:

@dp.message()
async def process_move(message: Message) -> None:
    if message.from_user.id not in boards:
        await message.answer("Нет начатой партии. Начните новую игру с помощью команды /newgame")
        return
    board = boards[message.from_user.id]

    try:
        make_move(board, message.text)
    except (chess.InvalidMoveError, chess.IllegalMoveError):
        await message.answer(
            "Некорректный ход. Пожалуйста, запишите ход в формате UCI "
            "https://ru.wikipedia.org/wiki/UCI_(%D0%BF%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB)"
        )
        return

    outcome = board.outcome()
    if outcome is not None:
        board_image = generate_board_image(board)
        await message.answer_photo(
            photo=BufferedInputFile(board_image, "game.png"),
            caption=f"Игра закончена! Результат - {outcome.result()}. Новая игра - /newgame"
        )
        del boards[message.from_user.id]
        return

    make_bot_move(board)
    outcome = board.outcome()
    if outcome is not None:
        board_image = generate_board_image(board)
        await message.answer_photo(
            photo=BufferedInputFile(board_image, "game.png"),
            caption=f"Игра закончена! Результат - {outcome.result()}. Новая игра - /newgame"
        )
        del boards[message.from_user.id]
        return

    board_image = generate_board_image(board)
    await message.answer_photo(
        photo=BufferedInputFile(board_image, "game.png"),
        caption="Текущее состояние доски. Делайте свой следующий ход"
    )

А также несколько функций в chess_utils.py:

def make_move(board: chess.Board, move: str) -> None:
    uci_move = chess.Move.from_uci(move)
    if not board.is_legal(uci_move):
        raise chess.IllegalMoveError
    board.push(uci_move)


def make_bot_move(board: chess.Board) -> None:
    bot_move = random.choice(list(board.legal_moves))
    board.push(bot_move)

Компьютерный соперник у нас пока что не очень умный; он просто выбирает случайный из возможных ходов. Архив с кодом Telegram-бота (без файла с токеном) можно скачать здесь.