Как я создавал Telegram-бота для текстовых квестов с использованием ChatGPT

Привет, SE7EN! Хочу поделиться своим опытом создания Telegram-бота для текстовых квестов при помощи ИИ. Если вы любите текстовые квесты, писать ботов или просто интересуетесь GPT, то этот материал для вас.

Ссылка на репозиторий с исходным кодом: questTg.

Как я создавал Telegram-бота для текстовых квестов с использованием ChatGPT
Так GPT видит логотип бота

Идея и вдохновение

В 6-м классе друг дал мне на книгу-игру «Адское болото». Это был текстовый квест — в каждой главе ты выбираешь действие главного героя. От выбора зависит на какую главу ты переходишь дальше.

Такая вот книга
Такая вот книга

Больше в текстовые квесты я не играл, но теплые воспоминания остались. Будучи взрослым была идея написать телеграм бота для квестов, но руки не доходили.

Все изменилось с появлением ChatGPT. Мне хотелось посмотреть на его возможности написав какой-нибудь проект с нуля. Ботов я до этого не писал, вот и решил реализовать старую задумку. Я использовал ChatGPT-4o.

Архитектура бота

Прежде чем писать код, я набросал примерную схему работы бота:

  1. Пользователь начинает игру — бот отправляет первую главу текстового квеста и вариантами ответа.

  2. Обработка выбора — пользователь выбирает один из вариантов, бот обрабатывает выбор и переходит к следующей главе.

  3. Повторение — процесс продолжается до тех пор, пока пользователь не завершит квест.

Кроме этого должен быть функционал работы с инвентарем, изменения характеристик героя (здоровье, удача и тп), сохранения и загрузки.

Плюс нужен сам файл квеста. Благо найти в интернете его оказалось не сложно.

Парсер

В найденном в интернете файле главы хранятся в текстовом видекё. Формат следующий:

:Prolog3
Inv+ Медное кольцо
PLN Как-то раз, следуя по древней Стезе Королей, ты набредаешь на скорчившуюся в пыли старую женщину. Ты переносишь её в тень раскидистой акации и даешь ей напиться из своей фляги. Вскоре она приходит в себя, но, желая убедиться, что с ней всё в порядке, ты решаешь проводить её до ближайшего города.
BTN Prolog4,Далее
End

Для удобства работы с файлом глав, мне нужно было написать парсер, который переводит текст в JSON:

"prolog3": 
[
  {
    "type": "inventory",
    "value": "inv+медное кольцо"
  },
  {
    "type": "text",
    "value": "Как-то раз, следуя по древней Стезе Королей, ты набредаешь на скорчившуюся в пыли старую женщину. Ты переносишь её в тень раскидистой акации и даешь ей напиться из своей фляги. Вскоре она приходит в себя, но, желая убедиться, что с ней всё в порядке, ты решаешь проводить её до ближайшего города."
  },
  {
    "type": "btn",
    "value": 
    {
       "text": "Далее",
       "target": "prolog4"
    }
  }
]

С этим у GPT проблем нет — он мастер регулярок и работы с текстом. Пишешь ему «Напиши парсер. Вот исходный формат, нужен вот такой» и он выдает то что нужно. Строки которые скрипт не спарсил я сохранял в отдельный текстовый файл — так я сразу видел, что надо добавить в обработчик.

На выходе получился полноценный JSON со всеми главами книги.

Переход по главам

Каждая глава представляет из себя список действий (actions) — отобразить текст, картинку, показать кнопки, присвоить значение переменной и т.п. Действия выполняются последовательно. Поэтому основной метод бота выглядит так:

def send_chapter(chat_id):
  state = get_state(chat_id)
  for action in chapter:
   execute_action(chat_id, state, action)
  send_buttons(chat_id, '.')

Тут state — состояние игрока (текущая глава, его характеристики и т.п.).
send_buttons — метод показа кнопок. После каждой главы мы должны отобразить кнопки. execute_action — метод исполнения действия (вывести текст, добавить кнопку). Выглядит так:

def execute_action(chat_id, state, action):
   action_type = action["type"]
   value = action["value"]
   if action_type == "text":
     handle_text(chat_id, value)
   elif action_type == "btn":
     handle_btn(state, value)
   elif action_type == "gold":
     handle_gold(state, value)
 ...

ChatGPT пока держится неплохо. Работа идет в формате «Напиши то, добавь это», и ты даже не погружаешься в работу кода — все само пишется, и даже работает. На какие-то мелкие ошибки ИИ сразу предлагает решение.

Добавляем инвентарь и характеристики

У игрока есть список характеристик (здоровье, сила и тд), и инвентарь (предметы которые он находит на протяжении квеста). Для этой задачи в JSON состояния пользователя я завел отдельные поля. Если в главе есть действие над характеристикой/инвентарем, значение поля меняется.

Также необходимо добавить соответствующие кнопки в меню.

Метод отправки кнопок меню выглядит так:

def send_buttons(chat_id, text="➡️"):
    state = state_cache.get(chat_id)
    markup = types.InlineKeyboardMarkup()
    # ✅ Add dynamic buttons (one per row)
    for button_text in state.get("options", {}).keys():
         button = types.InlineKeyboardButton(text=button_text, callback_data=button_text)
         markup.row(button)  # Добавляем каждую кнопку на отдельную строку

    # ✅ Add common buttons (two per row)
    common_buttons = [
        types.InlineKeyboardButton(text=text, callback_data=text)
        for text in config.COMMON_BUTTONS
    ]
    for i in range(0, len(common_buttons), 2):
        row_buttons = common_buttons[i:i + 2]
        markup.row(*row_buttons)
    bot.send_message(chat_id, text, reply_markup=markup, parse_mode="Markdown")

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

Строим интерфейс бота
Строим интерфейс бота

При нажатии кнопок отображаются характеристики и инвентарь.

Что по GPT? В этот момент эйфория от ИИ пропадает — при усложнении кода GPT начинает глючить, исправляет ошибку другой ошибкой, зацикливается. В какой-то момент ловишь себя на мысли, что надо погрузиться в код, и все переделать.

После того как садишься, сам продумываешь архитектуру и переписываешь большую часть кода, расширять функционал становится легче :.

Сохранение и загрузки

По аналогии с кнопками характеристик добавляем кнопки сохранения и загрузки игры.

Пишем обработчики кнопок. Так выглядит обработка сохранения:

@bot.callback_query_handler(func=lambda call: call.data == "📥 Save game")
def save_game(call):
    chat_id = call.message.chat.id
    save_state(chat_id)  # ✅ Сохраняем текущее состояние
    last_save = sorted(get_saved_states(chat_id))[-1]  # Берем последнее сохранение
    # Возвращаемся к текущим кнопкам главы
    send_buttons(chat_id, f"✅ Game saved: `{last_save}`")

Обработчики кнопок начинаются с @bot.callback_query_handler — мы проверяем факт нажатия кнопки, и, если кнопка называлась 📥 Save game, то запускаем исполнение функции.

Само сохранение представляет из себя запись переменной состояния пользователя в текстовый файл. Загрузка — считывание состояния из файла.

Мониторинг

Также я решил прикрутить гугл аналитику для бота. Эту часть я сделал без GPT, просто по инструкции.

Аналитика для бота
Аналитика для бота

Покрытие тестами

По мере роста количества строк кода, GPT начинает забывать старый функционал. Он или ломает старую логику, или просто удаляет часть методов. Когда я это понял, GPT уже сломал половину функционала бота. Поэтому без покрытия тестами с GPT работать опасно.

Что хорошо — GPT пишет тесты на ура. Даешь ему кусок кода, своими словами описываешь сценарий теста и вот модуль готов. Пример, тест нажатия кнопки, полностью сформированный GPT:

def test_btn(self):
        with patch("handlers.game_handler.config.chapters", test_chapters):
            with patch("handlers.game_handler.bot.send_message") as mock_send:
                action = {
                    "type": "btn",
                    "value": {
                        "text": "➡️ Продолжить",
                        "target": "test_end"
                    }
                }

                # ✅ Выполняем action
                execute_action(self.chat_id, self.state, action)

                # ✅ Проверяем, что inline-кнопка "➡️ Продолжить" появилась
                self.assertIn("➡️ Продолжить", self.state["options"])
                self.assertEqual(self.state["options"]["➡️ Продолжить"], "test_end")

                # ✅ Симулируем нажатие кнопки
                query = self.simulate_inline("➡️ Продолжить")
                handle_inline_choice(query)

                # ✅ Проверяем, что глава изменилась на test_end
                self.assertEqual(self.state["chapter"], "test_end")

        print("✅ Test passed!")

В итоге, каждый коммит я прогонял скрипт теста, проверяя что не сломался ни один из ранее реализованных сценариев.

Рисунки

Я хотел дополнить рисунки из книги сгенерированными при помощи GPT. Пример промпта (его я тоже составлял при помощи GPT):

draw image for text: Ты насылаешь на окружившую тебя Траву-Людоеда Огненное Заклинание. Охватившее траву пламя быстро затухает, но и этого оказывается вполне достаточно, чтобы освободить тебе путь к отступлению. Format: Black and white ink-style drawing with slight roughness and fewer details. Focus on simple outlines and minimal shading. Slightly uneven lines and rough texture for a hand-drawn feel. Fantasy theme with a dark, foggy background and twisted trees.

Рисунок.

GPT рисунок для главы
GPT рисунок для главы

Мне не особо понравилось, по 2-м причинам:

  • стилистика не совпадала со стилистикой авторских рисунков из книги

  • когда даешь GPT главу вырванную из контекста, то рисунок часто оказывается «не в тему».

Наверняка, оба эти момента можно решить поиграв с GPT (потенциал виден), но я не стал заморачиваться и оставил исходные рисунки автора..

Что можно улучшить

С GPT даже самые смелые задумки видятся вполне реализуемыми. Из идей расширения функционала:

  • Добавить интеграцию с Telegram Payment, чтоб создатели квестов могли принимать платежи

  • Расширить парсер для поддержи основных форматов квесто.

  • Добавить мультиплеер

  • Добавить динамические сюжеты (GPT сам генерит главы.

Общие впечатления

  • Смешанные чувства — с одной стороны GPT реально упрощает работу, моментально генеря куски кода. С другой стороны, у него еще много детских болезней, ему тяжело оперировать кодовой базой больше чем пара сотен строк. До высказываний что программисты не понадобятся еще далеко.

  • При работе с GPT код обязательно должен быть покрыт тестами.

  • Рисунки красивые, но без правильного промпта бестолковые.

  • С GPT появляется ощущение супермена — нереализуемого кода нет, все можно сделать за пару промптов. Часто так и есть. Можно легко погружаться в новую область, экспериментировать.

Поиграться с ботом можно по ссылке — https://t.me/QuestStroryBot

 

Источник

Читайте также