![](https://habrastorage.org/webt/hj/au/rk/hjaurkrxvjgxhdoj1-nf9mxgyxy.png)
Прежде всего, хочу поблагодарить Василия Рогина за превосходную реинкарнацию квестов «Космических рейнджеров». Исходные тексты его проекта также доступны по MIT-лицензии и сэкономили мне немало сил и времени.
![](https://habrastorage.org/webt/tp/ax/k8/tpaxk8fusogympd8t0c-9peljkm.png)
Стоит заметить, что TGE не так прост, как может показаться на первый взгляд. Например, при создании квестов активно используются формулы на специальном встроенном языке, позволяющем производить арифметические и логические действия над числовыми интервалами, а также инкапсулирующие в себе генерацию случайных значений. В этой статье можно найти некоторые подробности.
TGE позволяет создавать квесты, но был разработан довольно давно, содержит некоторое количество неприятных багов и продолжительное время не поддерживается. Именно здесь в нашей истории появляется Василий Рогин, выпустивший (помимо web-плейера) фактически новую версию графического редактора квестов.
![](https://habrastorage.org/webt/hq/rt/w7/hqrtw7bbmaeiykcyynrlluvqfge.png)
Это выглядит гораздо современнее, содержит меньше багов и, что самое главное, значительно удобнее в работе. Кроме того, web-редактор использует новый формат хранения (qmm) снимающий некоторые лимиты квестов (например, на максимальное количество параметров) и допускающий привязку медиа-элементов к сценам и переходам (раньше это приходилось делать отдельной программой).
![](https://habrastorage.org/webt/cz/zu/c9/czzuc9e0uwxh9zxpnxlpdrwlvmu.png)
Кстати, вот так выглядит в web-плейере сам игровой процесс.
На Хабре уже были статьи, посвящённые разработке Telegram-ботов, поэтому на эту тему повторяться не буду. Исходники здесь. Запускается всё в Node.js. В качестве базы данных используется PostgreSQL.
![](https://habrastorage.org/webt/zy/gw/cn/zygwcnisl3s3tzuaap-1qzqplsq.png)
Понимаю, что это может напугать, но вкратце расскажу про основные таблички. В users будут попадать все пользователи. Наиболее важные поля: user_id — идентификатор пользователя в Telegram, chat_id — туда будем слать сообщения, lang — язык (определяется автоматически, но потом можно поменять).
Сервис (приложение) может поддерживать сразу несколько Telegram-ботов, токены которых будем хранить в service.token (сам токен для бота можно получить у BotFather). Факт подключения пользователя к боту будет сохраняться в таблице user_service, поле is_developer которой будет включать некоторые полезные дополнительные команды.
Таблички command и action хранят сценарии. Это достаточно гибкий механизм для произвольного ввода/вывода, вызова хранимых процедур, REST-сервисов, а также запуска квестов. Сами квесты хранятся в script и привязаны к service. Язык квеста хранится в lang, благодаря чему не русскоязычные пользователи видят квесты на английском языке (которых правда сильно меньше):
![](https://habrastorage.org/webt/mj/by/ar/mjbyarhlcza3s7dueohnjxoxt5w.png)
Важной табличкой является user_context, в которой сохраняются контексты выполнения как квестов, так и обычных команд. К ней привязывается param_value, в которой сохраняются значения параметров (благодаря этому квест может быть продолжен даже после перезагрузки сервиса). В свою очередь, global_value сохраняет значения между запусками команд и квестов (там хранится статистика запусков, побед и поражений, а также кредиты пользователей, используемые в некоторых квестах).
Начать было решено с обратной связи. Действительно, к чему заморачиваться с почтой, когда под рукой есть Telegram? Конечно, каждый пользователь общается только с ботом, но что нам мешает передавать сообщения другим пользователям? Например, сообщения обычных пользователей передавать администраторам, а сообщения администраторов всем пользователям с совпадающими языковыми настройками. Конечно, помимо широковещательной рассылки, полезно дать администраторам возможность адресного ответа на какой-то вопрос пользователя:
![](https://habrastorage.org/webt/mi/ac/wd/miacwdqnjcs4skn9scrgwtevaeo.png)
Попробуем разобраться, что здесь произошло:
- Администратор написал в чат сообщение «Вопрос»
- Поскольку администратор является пользователем с подходящей языковой настройкой, он получает продублированное сообщение...
- И может на него ответить средствами Telegram
- В результате чего получает адресный ответ на своё самое первое сообщение
![](https://habrastorage.org/webt/fy/hj/xu/fyhjxu5odk8_cude0mftprahuo0.png)
Теперь, если установлено поле reply_to_message, мы можем найти исходное сообщение и ответить на него.
Посмотрим как выполняется запуск квеста. Можно выполнить команду "/start" с указанием идентификатора квеста, передав её в чат или просто перейдя по ссылке.
![](https://habrastorage.org/webt/ll/fe/v8/llfev8uv1mdwhpn77-vubayicd4.png)
Далее, последовательно выполняются все действия, составляющие команду. После этого контекст удаляется в setNextAction. Рассмотрим действие запуска квеста более подробно:
- Получаем id квеста из параметра командной строки
- Читаем описание скрипта из таблицы script (здесь нам требуется имя файла)
- Далее загружаем файл из каталога upload (разбирая его в процессе)
- Здесь же инициализируем параметры (и впервые сталкиваемся с формулами)
- Закрываем предыдущие контексты квестов если они есть
- И создаём новый контекст квеста, добавляя его в кэш (теперь квест может быть продолжен даже после перезагрузки сервиса)
- Некоторые квесты используют специальный параметр, помеченный флагом «Деньги игрока». Читаем значение этого параметра из global_value, используя привязки
- Выводим текст приветствия квеста, если он есть
- И наконец собираем первый диалог квеста для передачи его пользователю
Следует отметить необходимость двойного кэширования. Прежде всего, разбор qm-файла — дорогостоящая операция, разобранный же квест может занимать большой объём памяти. По этой причине, бот хранит в памяти не более пяти разобранных файлов. К счастью, эти данные не изменяются после разбора, могут совместно использоваться несколькими пользователями и их выгрузка не сломает выполнение квеста, а всего лишь приводит к ещё одному дополнительному разбору.
Второй уровень — это контексты, которые надо хранить все, благо они не такие большие как сами квесты. Здесь необходимо фиксировать в БД изменения локаций и параметров, на тот случай, если выполнение квеста придётся продолжить после перезагрузки сервиса.
Далее, выполняем квест, выбирая соответствующие пункты меню. Обратите внимание, что меню после активации одного из его пунктов удаляется, прежде всего по той причине, что меню, оставшееся в чате вне контекста его выполнения — не самая лучшая идея. Выбор меню обрабатывается как callback_query.
![](https://habrastorage.org/webt/kg/zg/zm/kgzgzmnxbdlflqgps1jght4lyl0.png)
![](https://habrastorage.org/webt/xh/yf/zx/xhyfzx2rpixafx66e2y9fwiqnt0.png)
Две локации по восемь вариантов в каждой. И это скорее правило чем исключение. Телеграм имеет свою специфику (например, не любит длинные тексты в переходах от одной локации к другой). Практически все квесты приходится редактировать, если только они не создавались под Telegram изначально.
Меню переключения языка сделано чуть сложнее. Прежде всего, эта команда отображается в меню (для этого она прописана в таблице command с установленным флагом is_visible).
![](https://habrastorage.org/webt/ra/pf/qp/rapfqpulclx54sfnecyydpmqeng.png)
В результате ввода команды открывается меню:
![](https://habrastorage.org/webt/79/mq/wv/79mqwvtfo8cmyijcpuwtxt77nce.png)
![](https://habrastorage.org/webt/hv/lp/bt/hvlpbtf41elkjew1ocg7qwxqhzy.png)
Можно заметить, что это дерево. Дочерние узлы скрипта привязаны к родительским через parent_id. Выполняемые узлом действия определяются значением type_id и обрабатываются здесь.
Значение type_id=5 определяет меню (в поле width можно указать ширину, то есть количество пунктов размещаемых на одной строке). Заголовок берётся из таблицы localized_string (обратите внимание, что он привязан к языку пользователя). Аналогичным образом из дочерних узлов собираются наименования пунктов меню. В качестве идентификаторов берутся id дочерних узлов и меню передаётся пользователю.
Тип дочерних узлов type_id=1 — это просто папка содержащая другие действия. Ищем в глубину первую не папку и добираемся до type_id=6 — вызова хранимки setLang. Здесь всё просто: меняем значение поля lang в табличке users. Параметры вызова описываются табличкой request_param.
По хорошему, следовало бы ещё вывести сообщение об успешном переключении языка, а также попытаться переключить текущую локацию квеста на другой язык (если такое возможно), но это я оставил на будущее.
Главная команда этого бота конечно другая: /quest выводит список подготовленных для загрузки квестов и обеспечивает их запуск. При этом анализируется язык пользователя. Фокус здесь в том, что эту команду можно выполнять как без параметров, так и указав имя квеста:
![](https://habrastorage.org/webt/mb/_2/j0/mb_2j0ukzyhdb7rrvum_8yisqmy.png)
![](https://habrastorage.org/webt/r3/4g/lf/r34glfpeini66czmzk3uhstrt2c.png)
Если найдено более одного квеста (например при вызове команды без параметров), в result_code помещается значение 1 и обработчик вызова хранимой процедуры переключает управление на дочерний узел, выводящий меню выбора квеста.
![](https://habrastorage.org/webt/db/a3/ux/dba3uxwi3d94yiqgd6vbel-hpeo.png)
В противном случае (если дочерний узел не найден), setNextAction идёт дальше и мы попадаем на узел запуска текстового квеста (такой же, как в обработчике команды /start). Параметр 5, на этот момент, уже содержит найденный ранее id квеста.
Как уже упоминалось выше, отображение квестов в Telegram имеет свою специфику. Например, вряд ли имеет смысл выводить картинки в каждой локации и переходе. В некоторых случаях обойтись без картинок никак нельзя (как в квесте "Дальнобойщик"), но это скорее исключение чем правило.
![](https://habrastorage.org/webt/wj/zx/8z/wjzx8z1tnuwnzkcn03j1s1q-ohm.png)
В некоторых случаях, текст этих диалогов важен и его полезно иметь перед глазами в дальнейшем. По этой причине, после удаления, текст диалога обычной локации выводится снова, уже без меню. Но это ещё одно сообщение, которое может спровоцировать срабатывание антиспам-фильтра! Чтобы таких сообщений было меньше, тип промежуточных узлов квеста следует заменять на «пустой» (диалоги всё равно будут выводиться, но не будут дублироваться после удаления).
Если антиспам всё таки сработал, может получиться так, что очередной диалог квеста, который сервис бота будет считать уже отосланным, на самом деле не попадёт в ленту чата (и пользователь не сможет продолжить квест). Чтобы такого не происходило, все подобные отсылки оборачиваются в функцию send, фиксирующую факт этой отправки, а также выполняемые после неё некоторые действия. Если идентификатор переданного сообщения не был получен, сообщение будет продублировано по расписанию фоновым процессом.
Пользователи могут загружать свои изображения и квесты. Здесь следует понимать, что все загружаемые файлы будут переименованы:
![](https://habrastorage.org/webt/n4/ta/is/n4taisgg7uiqg-yesvpy8k8cfx0.png)
Делается это для того, чтобы избежать конфликта имён файлов, загруженных разными пользователями. Исходные наименования загружаемых изображений сохраняются в привязке к пользователям и могут быть использованы в загружаемых ими квестах. Что касается самих загружаемых квестов, в общем доступе (до согласования с администратором) они не появляются, но могут быть загружены их владельцами в режиме отладки.
- /load file_N.qmm — Загрузка сценария квеста аналогична команде /start, но вместо id квеста указывается имя загруженного файла сценария (после переименования). В отличии от команд /start и /quest, команда /load не создаёт контекст выполнения квеста в базе данных и, как следствие, не изменяет значений глобальных счётчиков пользователя (статистики) в результате выполнения квеста (подкрутить таким образом кредиты тоже не получится).
- /show id — Отображает в чате идентификатор текущей локации квеста.
- /set pN V — Присваивает заданное значение параметру квеста. Например, если кредиты внутри квеста сохраняются в параметре p1, улучшить благосостояние поможет команда "/set p1 10000".
- /calc formula — Очень полезная команда, вычисляющая значение формул квеста. Текущие значения параметров учитываются, поэтому для просмотра значения параметра p1, например, достаточно выполнить команду "/calc [p1]", а формулы из редактора квестов можно просто копипастить в чат.
Помимо этого, есть ещё одна возможность, доступная всем пользователям. Команда /save может быть полезна для прохождения сложных квестов.
![](https://habrastorage.org/webt/gv/gl/wj/gvglwj7fguzwx8oee_e5zt12twa.png)
Сохраняем qms-файл себе на диск, а потом, если квест пошёл как-то не так, кидаем его обратно в чат:
![](https://habrastorage.org/webt/pw/vt/l8/pwvtl8dpi3fssjc8ot_dflqbuyi.png)
Важный нюанс заключается в том, что загруженный таким образом квест не влияет на статистику пользователя (очки и кредиты таким образом накрутить невозможно).
![](https://habrastorage.org/webt/vb/ag/4c/vbag4cjlpc0lhzpj8snm--k1evs.png)
![](https://habrastorage.org/webt/lh/vw/fn/lhvwfnzuewrzx4smkxsxcphup58.png)
![](https://habrastorage.org/webt/5s/6l/kr/5s6lkrf6ixdcf7klnotspxjuuaa.png)
![](https://habrastorage.org/webt/2q/g4/ze/2qg4ze6gcgqhpi_ke93lar6pbrq.png)
![](https://habrastorage.org/webt/5w/f1/b4/5wf1b425duxjwtnbekcyociisv0.png)
![](https://habrastorage.org/webt/ri/b3/vo/rib3voyhwhmpzf40ebvimqpailm.png)
![](https://habrastorage.org/webt/5h/z8/0f/5hz80f7fndbsqovajhcq7j6blnu.png)
![](https://habrastorage.org/webt/bc/0y/7-/bc0y7-ojjc5mfmf9df7jqv19rcs.png)
![](https://habrastorage.org/webt/9m/kn/zu/9mknzurlc1usptgx8gqwwtppmui.png)
Разумеется, это далеко не всё. Есть куча мелких квестов, в которых можно поиграть в сложную малокскую игру (есть на английском), очень маленькую манкалу, попытаться вскрыть сейф из "Братьев Пилотов", поперекладывать "Ханойские башни" и даже поиграть в домино. И разумеется, квесты будут ещё добавляться.
Что дальше? Текстовые квесты «Космических рейнджеров» в Telegram — это уже прекрасно, но что если дать возможность играть в Цитадели или в тех же Роботов не против квестов, а друг с другом? Организовать, своего рода, квестовую арену? Разумеется, сценарии придётся полностью переделывать. Что касается самого движка…
![](https://habrastorage.org/webt/sy/hz/wt/syhzwtzeigjysfetrhywcqzlod0.png)
Переход будет выполняться автоматически, как и при отсутствии заголовка. Попутно (при проходе через !session) будет производиться подключение пользователя к сессии.
![](https://habrastorage.org/webt/wz/vb/5h/wzvb5hm3fd3eerkhwwx-xsdzb_s.png)
Первый пользователь, прошедший через !session, создаст сессию (в таблице session), а следующий к ней подключится (user_session). В сессии могут участвовать и более двух игроков (как например, в игре Перудо). Обратите внимание, что к одной записи session_type могут быть подключены несколько скриптов. Это позволит соревноваться игрокам с различными языковыми настройками.
Минимальное и максимальное количество игроков задаётся в session_type. Там же определяются параметры, через которые будет осуществляться взаимодействие. Прежде всего, это start_param, определяющий номер параметра, с которого будут начинаться блоки синхронизации. Поле param_count будет задавать размер блока (количество параметров передаваемых каждым пользователем). Нулевой блок предлагается использовать для задания исходящих значений параметров.
![](https://habrastorage.org/webt/-3/rs/wo/-3rswovaogwog3b5g1hhnrzqhdw.png)
Здесь начинается магия. Прежде всего, это единственное место, в котором игроки ждут друг друга (в !session также может указываться таймаут на подключение в миллисекундах, но там ожидание производиться не будет). В первый раз выполнив !wait, игрок будет ожидать наполнения сессии (в соответствии с настройками), в течении суммарного таймаута обеих команд.
Если наполнения сессии дождаться не удастся — ничего страшного, !wait сбросит параметр индекса игрока (заданный в session_type.index_param). В нашем случае, это параметр p1. После чего, каждый игрок продолжит игру независимо друг от друга, уже против бота:
![](https://habrastorage.org/webt/tq/gs/tx/tqgstxhkmj4hpyapg7qgesfnsak.png)
Что произойдёт если ожидание всех игроков завершится удачно? Перед выполнением !wait, каждый из игроков заполнит исходящие параметры в нулевом блоке. Задача !wait — переложить параметры каждого игрока в соответствующий по счёту блок синхронизации, после чего «пробудить» всех игроков, продолжив квест. Для дополнительного удобства, определяются строковые подстановки <1>, <2> и т.д., заменяющиеся именами соответствующих игроков.
![](https://habrastorage.org/webt/a-/mx/px/a-mxpxekzjp8up3gaqiotq-t5as.png)
В общем, как концепт, это вроде работает, но работы по отладке предстоит ещё очень много (и отлаживаться с двух сессий в Телеграмме до чёртиков неудобно). Таймауты пока не реализованы и с партнёром надо предварительно договариваться, чтобы он присоединился между «Конечно, я приму участие в игре» и самым первым ходом (Камень, Ножницы, Бумага, Ящерица или Спок). В противном случае, противником будет назначен бот.
![](https://habrastorage.org/webt/qm/y_/2h/qmy_2h0xvwr8rvcgk9fwmr0-aks.png)
Такой вот задел на будущее…
Комментарии (12)
joffer
29.07.2024 09:50хм, атк если так подумать - можно заморочиться и реализовать какую-то карточную игру в телеге по типу мтг (Magic The Gathering)... в теории, это можно как-то реализовать и потом, допустим, опять же в каком-то чате собирать возможных участников - чтобы можно было друг с другом играть...
ts347
29.07.2024 09:50Каждый раз, когда я вижу, как кто-то пытается удалить гланды через Телеграм, у меня возникает только один вопрос – зачем? Зачем привязываться к одной ограниченной платформе и затем героически преодолевать ее. Неужели сделать простой сайт настолько сложнее...
Fen1kz
29.07.2024 09:50Предполагаю, что если у вас игра/бот взлетит, то можно будет довольно легко добавить рассылку рекламы раз в х дней и получать пассивный доход.
Если у вас взлетит сайт, то получите вы расходы на хостинг и собственно всё.
Поэтому все так с телеграм-ботами для всего и носятся.
GlukKazan Автор
29.07.2024 09:50Конкретно этот бот останется бесплатным и без всяких подписок. И без рекламных рассылок тоже (информационные будут).
anna_meow
Идея так то классная, если бы ещё можно было с незнакомцами онлайн играть, допустим)
GlukKazan Автор
Над этим и работаю (в самом конце статьи)