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

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

Больше в текстовые квесты я не играл, но теплые воспоминания остались. Будучи взрослым была идея написать телеграм бота для квестов, но руки не доходили.
Все изменилось с появлением ChatGPT. Мне хотелось посмотреть на его возможности написав какой-нибудь проект с нуля. Ботов я до этого не писал, вот и решил реализовать старую задумку. Я использовал ChatGPT-4o.
Архитектура бота
Прежде чем писать код, я набросал примерную схему работы бота:
-
Пользователь начинает игру — бот отправляет первую главу текстового квеста и вариантами ответа.
-
Обработка выбора — пользователь выбирает один из вариантов, бот обрабатывает выбор и переходит к следующей главе.
-
Повторение — процесс продолжается до тех пор, пока пользователь не завершит квест.
Кроме этого должен быть функционал работы с инвентарем, изменения характеристик героя (здоровье, удача и тп), сохранения и загрузки.
Плюс нужен сам файл квеста. Благо найти в интернете его оказалось не сложно.
Парсер
В найденном в интернете файле главы хранятся в текстовом видекё. Формат следующий:
: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.
Рисунок.

Мне не особо понравилось, по 2-м причинам:
-
стилистика не совпадала со стилистикой авторских рисунков из книги
-
когда даешь GPT главу вырванную из контекста, то рисунок часто оказывается «не в тему».
Наверняка, оба эти момента можно решить поиграв с GPT (потенциал виден), но я не стал заморачиваться и оставил исходные рисунки автора..
Что можно улучшить
С GPT даже самые смелые задумки видятся вполне реализуемыми. Из идей расширения функционала:
-
Добавить интеграцию с Telegram Payment, чтоб создатели квестов могли принимать платежи
-
Расширить парсер для поддержи основных форматов квесто.
-
Добавить мультиплеер
-
Добавить динамические сюжеты (GPT сам генерит главы.
Общие впечатления
-
Смешанные чувства — с одной стороны GPT реально упрощает работу, моментально генеря куски кода. С другой стороны, у него еще много детских болезней, ему тяжело оперировать кодовой базой больше чем пара сотен строк. До высказываний что программисты не понадобятся еще далеко.
-
При работе с GPT код обязательно должен быть покрыт тестами.
-
Рисунки красивые, но без правильного промпта бестолковые.
-
С GPT появляется ощущение супермена — нереализуемого кода нет, все можно сделать за пару промптов. Часто так и есть. Можно легко погружаться в новую область, экспериментировать.
Поиграться с ботом можно по ссылке — https://t.me/QuestStroryBot