при помощи Crystal, Lucky, Tourmaline и Telegram Bot Gaming Platform
Физикам можно сразу в репу.

Как известно(не многим), программист, хотя бы раз в жизни должен – поломать прод и выхватить за это, починить его, а на досуге построить баню и написать игру.
Успешно выполнив первые пункты пришла пора перейти к последнему из них, чтобы пить заслуженное пиво в бане, ни на что уже более не отвлекаясь.
Просто писать игру достаточно скучно и как и миллиард авторов до меня, я решил сделать такую игру – в которую интересно будет сыграть, хотя бы мне самому и не меньше двух, а то и трёх раз.
К моменту принятия решения, закончились новогодние выходные, друзья мои разъехались и я подумал:
-
для усложнения задачи неплохо было бы воплотить эту самую игру в телеге.
-
игра должна многопользовательской — как минимум для пары человек.
-
и почему бы не нарды — мы с удовольствием в них рубимся время от времени?
Время на проект (для борьбы с прокрастинацией) выделил себе ровно месяц. Так появилась задача и сроки.
И если, для ее решения, мне нужны инструменты, то почему бы не взять какой нибудь новенький, сверкающий молоток моей мечты, который я давно хотел применить но не было повода взять его в руки?
Итак – Crystal поскольку, я решил, что он прекрасен, удобен и быстр. (спойлер: да, так и есть)
Копание в Telegram Bot Gaming Platform несколько разочаровало. Довольно унылые примеры с HTML+JS на тему: нажми кнопку быстро/вовремя или считай в уме быстрее всех, не вдохновляли. Копание в исходниках тележки дало некоторое понимание процесса взаимодействия ее с игрой. Документация на эту тему – довольно мутная.
Сценарий взаимодействия получился такой – телеграмм кидает уведомление, о том что пользователь нажал кнопку «Играть в …» под сообщением с игрой. В ответ нужно отправить уникальную ссылку которую телеграмм откроет для пользователя – и да начнется битва)
Конечно я рассчитывал на что нибудь большее, вроде отправки сообщений через сервер tg для какой то коммуникации между участниками игры, но не случилось. Печально, зато можно поковырять websockets.
Что понадобится?
-
endpoint который будет обрабатывать сообщения телеги
-
еще (как минимум один) который собственно и будет отдавать игровую доску
-
нечто, имеющее роутер для обработки запросов, средства собрать html страницу и возможность создавать/хранить/доставать активную игру в базе. Словом небольшой framework который избавит меня от рутины и освободит для высокого)
Выбор мой пал на Lucky (на тот момент версии 0.24) по следующим причинам:
-
подход создателей и философия проекта
-
хорошая документация
-
механизм передачи параметров от контроллера к визуализации
-
наличие готовых json api контроллеров нужных для api телеги
-
хороший роутер и хелперы к нему
-
более менее приемлемый интерфейс для работы с БД
-
наличие готового деплоя на Heroku
-
всё лишнее можно отключить
-
всё отсутствующее дописать
Создание игровой доски в примитивном виде.

Доска была нарисована в Inkscape и содержит несколько слоев. Сама доска, фишки, кости, слой сообщений. В процессе конечно были сделаны всевозможные ошибки относительно размещения начала системы координат, что важно для использования transform rotate преобразований в SVG.
В приложении, доска представляет из себя SVG, – генерируемый кодом на Crystal при помощи Lucky конечно. Большую часть нудной работы сделал converting HTML to Lucky methods, (в него я загнал svg из Inkscape) остается только убрать повторения и разбить структуру svg на логические элементы с которыми будет удобно работать.

В результате из 50kb SVG получилось 2-3 сотни строк на Crystal лежащие в соответствующей page. Время на вывод такой страницы без полезной нагрузки на моем буке измеряется µs. Я не стал разносить всё по компонентам исключительно экономя время. Многие возможности Crystal были просто не использованы. На тот момент степень понимания языка ещё не давала мне использовать все его преимущества. К концу проекта глядя на этот код я понимал, что многое можно сделать на много, на много красивее и удобнее.
Логика игры, правила и ограничения.
Теперь когда есть на чем играть, реализую логику – все правила и состояния игры, в одном классе Game, лежит в src/models/game.cr. Примерно 600 строк. Проследить боль и страдания можно в тестах к этому классу до теста 399 строки когда оно смогло играть с собой до победы. Правила брал здесь
Совмещение логики и визуализации игры
Дальше я начал натягивать сову на глобус. Совмещать логику и визуализацию, цель: отображать любое сохраненное состояние игры на доске.
После победы(сова сопротивлялась как могла) я двинул дальше, Надо добавить управление. Детектировать, что ход сделан и отправлять его на сервер в соответствующий action.
Это удобнее было сделать на JS. Я не фанат JS, но когда надо – тогда надо.
Lucky уже настроен для использования всей этой лабуды с Webpack и.т.д. По умолчанию подключены небольшие и довольно полезные Turbolinks и rails-ujs. Выпиливать их не стал. JS в проекте Lucky лежит в src/js/app.js
Управление простое, выбираем кость и фишку которой ходим либо наоборот и отправляем ход на сервер, позднее добавил возможность ходить выбирая фишку и место назначения.
Доверять полученному со стороны клиента (без должной проверки) нехорошо. Поэтому, работает только логика на сервере в классе Game. Состояние игры всегда корректно, можно спокойно выйти в другой чат и ответить на сообщение, и без проблем снова открыть игру на том же самом месте. Смычка города с селом, происходит в этом action. По сути это один большой case связывающий действия пользователя с состоянием игры. Если изменение было игрок увидит его после выполнения перенаправления на action отображения игровой доски.
На этом же этапе я подключил Turbolinks и rails-ujs для плавного апдейта страницы.
Настало время подключать Telegram.
Чтобы тестировать игру в связке с телеграмм нужен сервер с IP или доменом и соответствующим SSL сертификатом для подключения Webhooks через который телеграмм будет слать updates приложению.
Сервис размещаю на Heroku. Процесс настройки деплоя сводится к нескольким тривиальным командам, после прочтения главы документации Lucky на эту тему. Heroku работает с телеграмм без возни с сертификатами сразу из коробки.
Режим бота для доставки игры выбрал inline mode как наиболее прогрессивный и безопасный.
Быстро набросал клиент для взаимодействия с api телеграмм, пара часов отладки и всё заработало. После чего, естественно, обнаружил прекрасный шард реализующий работу с api телеграмм для Crystal и имя ему Tourmaline. У него был всего один недостаток он не умел игры, я это поправил и автор оперативно принял изменения. Немного перетряхнул код и встроил бот уже через Tourmaline.
Схема работы приложения с api телеграмм подробно:
-
создание игрового бота, всё стандартно через @BotFather
-
action в Lucky который будет принимать updates от телеграмм.
-
прописать url этого action с помощью setwebhook в телеграмм api. (Tourmaline позволяет динамически устанавливать webhook, но я предпочел самостоятельно контролировать этот процесс)
-
при регистрации игры выдается ссылка вида game link (e.g., t.me/bot?game=game) в моем случае http://t.me/tavla_best_bot?game=tavla. Для начала, достаточно этой ссылки чтобы поделится игрой. Клик на нее предложит выбрать чат, куда будет отправлено сообщение с игрой.
-
Под сообщением, по умолчанию будет одна обязательная кнопка «Играть в…». Клик пользователем по этой кнопке, отправляет на action (привязанный в пункте 3 к Webhook) структуру Update c вложенной в поле
callback_queryеще одной структурой с оригинальным названием CallbackQuery. Из нее берется полеgame_short_name– имя вызываемой игры (если у нас ссылка вида http://t.me/tavla_best_bot?game=tavla значение должно быть «tavla») и второе полеcallback_query.idнужно передать обратно в answerCallbackQuery, чтобы телега понимала на что отвечаем.
Tourmaline активно использует аннотации, что очень удобно в написании бота, но не всегда удобно искать где отработает соответствующая функция. Из action апдейт прилетит сюда.

В соответствии с 5 пунктом соглашения при создании игрового бота не получится хранить куки и создавать сессии – поэтому все нужные параметры должны быть в url который генерирую в ответе. Фактически это идентификация игры и пользователя по динамическому url. Проверять такие url надо самостоятельно, поэтому использую helper и mixin.
Если всё прошло успешно, телеграмм клиент открывает в браузере встроенном или внешнем переданный ему в answerCallbackQuery url.
Параметры user сохраняются в базу при необходимости. Я использую в проекте Postgres скорее по привычке и для потестить ORM Avram, чем по необходимости, а вообще, вполне хватило бы Redis.
Выглядит url так (разделение по двойному тире): /active_games/1eae03de19d1e909207c6192baff500771e0cbeb—AgzzzzzzzzzzzzzzUWfH7r74—321232123—5345353343
Первая часть подпись, генерируется из остальных параметров и некоего salt. Вторая часть inline_mesage_id. Третья и четвертая id пользователя телеграмм. Четвертая часть не обязательна, в этом случае за второго игрока станет действовать бот.
Идентификаторinline_mesage_id однозначно определяет сообщение с игрой по которому сделан клик, user_idтого кто кликал.
На этом работа телеги пока заканчивается и я возвращаюсь к игровой странице.
Получив запрос get c url и проверив его валидность смотрю есть ли в базе такая игра. Если есть отображаю ее, если нет — сначала создаю.
Общая логика такая (здесь) – если два человека нажали в чате на одном сообщение кнопку играть, до того как игра начата одним из них, игра будет человек – человек, в противном случае человек – бот. Естественно схему можно менять как угодно, вплоть до рейтинга игроков и поиска активных в данный момент соперников не имеющих общих чатов.
Последние штрихи
Добавил в middleware Lucky websocket обработчик для доставки обновлений. Когда противник сделал ход, игра обновит и доску второго игрока. На тот момент ws actions в Lucky был только в виде пары коммитов в экспериментальной ветке, но это всё равно было немного не то, что мне нужно.

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

Добавил боту возможность отправлять игру через inline запрос. Для этого в строке чата набираем имя бота @tavla_best_bot в ответ появится подсказка с игрой на которую нужно кликнуть.
Добавил меню c кнопками запуска игры, для комплекта если кто то запустит бот напрямую.
Добавил отправку набранных игровых очков в телеграмм.
На этом процесс можно было считать законченным. Оставшееся время я потратил на визуальщину и вкусовщину. Frontend не является моей специализацией, поэтому прошу отнестись снисходительно. Многое наверняка можно было сделать красивее и проще.
Проблема оставшаяся нерешенной из скупердяйства принципиальных соображений – вертикальный режим в каких то (не всех) Iphone/Ipad, – ломает верстку. Я не пользуюсь этими девайсами, а реально бесплатных тестовых сред для разработчика я не нашел. Если кто то пофиксит буду рад.
Ps
Код выложен в том виде к котором был на момент окончания времени проекта, он полностью рабочий, следуя инструкции в Readme можно поднять свой инстанс игры, естественно бот тоже нужен свой.
Мест приложения рук, еще довольно много, например добавить боту игры ума. Возможно со временем я сделаю порт TDBackGammon и бот будет красавчик, но пока времени на это нет.
Всего на проект я потратил почти 75 часов за отведенные 30 дней, в среднем примерно по 2.5 часа в день, самый длительный непрерывный интервал 9.5 часов.
Pps
Еще момент который касается бесплатных инстансов Heroku, они засыпают при неактивности, поэтому, отклик которого телеграмм ждет от приложения очень быстро, может не успеть с первого раза. Чтобы избежать подобной ситуации я дергаю инстанс снаружи HEAD запросом 1 раз в минуту, при таком подходе времени бесплатной активности вполне хватает на месяц для всех, кто сейчас играет в tavla. Надеюсь хабраэффекта не случится, и я и дальше смогу спокойно рубиться в нее за завтраком:)
Моя искренняя благодарность всем, кто делает и помогает делать Crystal и его экосистему. Мне очень понравился Crystal. Я сделал для себя определённые выводы относительно будущего этого языка и постараюсь сделать на нём еще несколько проектов, не важно по работе или ради собственного удовольствия. Спасибо всем, кто это осилил, успехов и удачных проектов!
ссылка на оригинал статьи https://habr.com/ru/post/567064/
Добавить комментарий