Снова говорим с Сергеем Опиваловым, Senior Software инженером в Gradle. Обсуждаем его пет-проект — приложение ATHYLPS.
Сергей, что такое ATHYLPS?
ATHYLPS это аббревиатура от App That Helps You Learn Poker Stuff. Изначально было техническим названием проекта, которое перекочевало в прод. Однозначно соглашусь, что читается непросто и плохо запоминается, но планов по ребрендингу пока что нет.
Итак, ATHYLPS — это приложение-тренажер для отработки математических навыков в покере, таких как как подсчет аутов, подсчет шансов на закрытие комбинации, подсчет шансов банка и некоторых других. Сама покерная математика довольно хорошо алгоритмизируется (обсудим подробнее далее), что и легло в основу приложения.
Какова история его возникновения?
Лично я к покеру отношусь довольно нейтрально. Как и мои друзья, кто участвует в разработке ATHYLPS. Но изначально идея была одного из моих коллег-менеджеров, который увлекается покером. Я случайно увидел у него скетчи приложения, и поинтересовался — что это? Он описал идею, но возможностей для разработки у него не было. Я взял паузу, и пошел с этой идеей к друзьям. Мы как раз находились в поиске нового вдохновения для пет-проекта (не релизнув ни одного из предыдущих, разумеется), и идея ATHYLPS показалась нам очень привлекательной: целевая аудитория очень узкая и очень понятная, многие математические модели хорошо ложатся на код, есть понятный бэклог до первого релиза.
Договорившись о сотрудничестве, автор идеи стал product менеджером на проекте, а я — главным программистом в команде с моими друзьями. И тогда мы заказали UI дизайн для приложения.
Long story short, мы зарелизили 1.0 для Android. У нас не было никаких идей касаемо маркетинга, мы не закупали никакой трафик, все что у нас было после релиза — более менее качественное ASO, которое сделало нас Топ 1 в выдаче по некоторым запросам в поиске Google Play. Таким образом, количество установок не взлетело драматически после релиза, а скорее росло органически, но очень скромными темпами.
Первые три месяца после релиза выглядели примерно так:
Параллельно с релизом 1.0 для Android, мы стартовали разработку iOS.
Почему первоначально для проекта был выбран Flutter?
Прагматичный выбор. 100% наших инженеров (3 человека) это JVM/Android разработчики. Никто из команды не хотел смотреть в нативный iOS для расширения кругозора (по религиозным причинам :)), а вот Flutter выглядел привлекательно. Еще раз подчеркну, Android приложение всегда было нативным, а вот iOS — написали на Flutter.
В первой версии у вас было ядро на Go. Не кажется ли это уже избыточным для пет-проекта?
Тут нужно больше исторического контекста. С самого начала мы знали, что приложение будет на двух платформах. Также мы были уверены, что логическое ядро с генерацией упражнений должно быть одним для обеих платформ.
В то время(2019) мы видели несколько опций:
- С++
- Унести логику на бэкэнд
- Kotlin Native
Я был против перемещения этой логики на бэкенд — возможность решать упражнения только онлайн выглядело как ненужное ограничение для пользователей,
Kotlin Native тогда был довольно экспериментальным, но мы знали что он активно используется в некоторых проектах, типа Яндекс Карт, что добавляло оптимизма, и я решил взять его для реализации прототипа.
Нужно сказать о том, что должно делать ATHYLPS ядро:
- Декларировать покерные примитивы — карта, масть, ранк, колода, набор комбинаций и другое
- Оценка покерных рук. На вход принимается массив карт, на выходе — комбинация
- Сравнение рук. Две руки можно сравнить и сказать какая из них сильнее (или ничья)
- Доставать “значимые” карты из любой руки
- Генерировать неограниченное количество вариантов упражнений, основанных, по сути, на генерации рандомных рук и их эвалюации
Работа над прототипом ядра показала, что эвалюация (оценка) покерных рук — не самая тривиальная задача.
Мне не удавалось ее как либо адекватно решить, и я начал искать решения на GitHub. Оказалось, что это довольно популярная задача в классах Computer Science в американских университетах, и на GitHub много решений. Подавляющее большинство были написаны на C++, но в один момент попалось решение на Go, с очень подходящим API для наших целей. Параллельно мы узнали о Gomobile — инструменте для компиляции Go под мобильные платформы.
Взяв за основу это ядро, я покрыл его сверху несколькими слоями бизнес логики, и научил отдавать наружу то, что нужно клиенту.
Go очень производительный язык, но в попытках генерировать случайные варианты упражнений я делал с ним довольно спорные вещи.
Казалось бы, в карточной колоде всего 52 карты, и на таких цифрах можно вообще крутить вложенные друг в друга `while(true)`, но нет. Скажем, есть участки логики такого рода:
Берется случайная комбинация из списка, и генерируется под нее рука для игрока. Скажем, выпал нам Стрит из списка, мы берем колоду из 52 карт и случайным образом достаем оттуда по 5 карт, пока не найдем те, которые образуют Стрит. Этот шаг обычно быстрый. А теперь мы хотим сгенерировать руку для оппонента. Берем случайную комбинацию, но колоду теперь мы не можем взять новую, мы берем ту, которая осталась с первого шага. И вот из такой колоды достать нужную комбинацию бывает затруднительно. В этих случаях у нас есть специальные лимиты попыток, после которых мы либо меняем комбинацию, либо сбрасываем все состояние и перегенерируем упражнение снова в надежде на более удачный инпут.
Звучит супер-неэффективно, но эта оптимизация сделала генерацию упражнений на 2 порядка быстрее.
Осознав, что хорошо бы знать производительность ядра в проде, я завел простую телеметрию, которая замеряла время выполнения участка кода с генерацией каждого упражнения. По данным за 4 года, среднее время генерации упражнения ATHYLPS ядром равно 24мс. Но так как ядро общается с клиентской частью с помощью JSON, справедливо было бы добавить сюда еще и время marshalling/unmarshalling JSON.
Подводя итоги касательно Go — я бы не сказал, что его использование было избыточным. Как видите, это был выбор прагматика, не пожелавшего реализовывать эвалюацию покерных рук с нуля.
Кто сейчас работает над этим проектом?
Команда разработки все та же — я и двое моих друзей: Тимофей Плотников и Александр Ермоленко. Все Android инженеры, но Тимофей, помимо Android, имеет опыт с iOS, а Александр с Flutter.
Продуктовая команда поменялась — автор оригинальной идеи покинул проект после релиза 1.0, но к нам присоединились два практикующих игрока в покер (пожелавших остаться анонимными), один из которых является владельцем покерного фонда (информацию о явлении покерных фондов можно найти в интернете). Такое усиление продуктовой команды имело позитивный эффект: появился хороший vision того, как проект должен развиваться и что должен предлагать своим пользователям.
Почему потом произошел отказ от Flutter в пользу Kotlin Multiplatform?
Совокупность факторов. Flutter неплохо себя показывал на iOS, и поначалу были даже мысли о том, что Android тоже нужно мигрировать на Flutter. Персонально мне не слишком нравилось это решение, так как я считаю, что от разработки пет-проекта можно и нужно получать удовольствие. А удовольствия от использования Dart я получал не много :) Но, скажем так, меня можно было продавить на это решение.
Но тут пришел Jetpack Compose, KMP, а потом и Compose Multiplatform. Я смотрел на демки, мне нравилось, но я не испытывал фантазий как же хорошо это заменит нативный iOS во всех случаях. В то же время я был уверен, что ATHYLPS это хороший кандидат для миграции на Compose Multiplatform (как и на Flutter, справедливости ради), потому что у нас нет абсолютно никакого взаимодействия с платформой — все данные, что нужны приложению, мы генерируем сами.
Тут надо сказать о ядре на Go и его поддержке в течении жизни приложения. Фиксы в ядро всегда были не самой приятной вещью и требовали набора церемоний. Вот примерный список проблем, которые можно выделить:
- Код ядра лежал в отдельном репозитории
- Тулинг для разработки на Go был очень слабо мною осилен: я использовал VS Code без каких либо нормальных плагинов, без возможности дебажить, без возможности писать тесты (о которых я мечтал, откровенно говоря)
- Жесточайший bus-factor: никто в команде не представлял, как работает ядро, поэтому я был бутылочным горлышком при его разработке
Мы долгое время не пытались мигрировать ядро на Kotlin, потому что не до конца понимали, как работает эвалюация покерных рук. Вкратце, там используются битовые сдвиги, и содержится код такого типа:
// LexographicallyNextBitSequence calculates the next permutation of // bits in a lexicographical sense. The algorithm comes from // https://graphics.stanford.edu/~seander/bithacks.html#NextBitPermutation. func lexographicallyNextBitSequence(bits int32) int32 { t := (bits | (bits - 1)) + 1 return t | ((((t & -t) / (bits & -bits)) >> 1) - 1) }
И такого там прилично. Это, скажем так, не тот код, который я был готов рефакторить и мигрировать на другой язык программирования. Я был рад, что он просто делает то, что мне нужно.
Надо сказать, что сам по себе Go его стандартная библиотека выглядят очень примитивно по сравнению с Kotlin (специалисты по Go конечно поправят меня). Мне как-то нужен был Set в Go, я полез искать в stdlib и не нашел. Пошел спрашивать у уважаемых людей на Stackoverflow — сказали “используй Map”. Ok… Но я понимаю и сильные стороны языка, есть задачи, где у Go не так много альтернатив. Просто, видимо, я не был готов выходить из JVM парадигмы.
Итак, совокупность 3 факторов:
- Усталость от “ненативного” для команды языка
- Примитивность Go
- Наличие тестов на эвалюацию карт в библиотеке, которую я взял за основу ядра
позволила нам сделать эксперимент. Взяли и один к одному переписали Go на не идиоматичный Kotlin, где-то с ChatGPT (он хорошо справляется с таким задачами), где-то сами. Тесты тоже переписали. Тесты оказались зелеными, что дало уверенности в возможном успехе эксперимента.
После этого мы переписали все слои бизнес логики вокруг эвалюции карт на идиоматичный Kotlin. Написали очень подробные тесты, которых нам так не хватало, и тут же нашли множество мелких граничных случаев, в которых мы ошибались.
Так появилось ATHYLPS Core 2.0 на Kotlin. Теперь появилась уверенность, что мы можем и хотим держать всю кодовую базу Android + iOS + Сore в монорепозитории. Нам нужен был прототип iOS на Kotlin Multiplatform.
Получив приемлемо работающий iOS прототип (с этим не было больших проблем), мы приняли решение о миграции проекта целиком на Kotlin Multiplatform.
Kotlin Multiplatform готов к проду?
Зависит от того, какой у вас прод :) Для проектов типа ATHYLPS я почти уверен, что да, хоть мы еще и в середине процесса миграции. Я просто не вижу (пока) блокеров для нас. Я могу запускать бизнес логику на iOS? Да. Я могу рисовать на iOS? Да.
Для больших проектов я бы не торопился, наверное, заскакивать в этот поезд.
Но в то же время с KMP есть опция постепенно мигрировать вашу кодовую базу, что может иметь свои плюсы. Если у вас нативный Android + нативный iOS, иметь растущее количество shared кода, но оставаться гибким в местах где нужна платформа — хорошая идея.
По опыту — когда выбирать Flutter, а когда KMP?
Сегодня экосистема Flutter выглядит более “взрослой”, но и KMP разрабатывается быстрыми темпами. Когда-то в минусы Flutter записывали лагающие анимации на iOS и необходимость их “прогрева”. Проблема была в Skia, который Flutter использовал для рендеринга. Сегодня Flutter использует Impeller, в котором нет проблемы с анимацией.
Но что интересно, и что вызывает вопросы, почему KMP/Compose Multiplatform сегодня все еще использует Skia для рендеринга, зная о проблемах с анимациями на iOS?
Подробнее, чем от меня, о проблемах обеих технологий читатели могут узнать из дебатов, прошедших недавно между командами, использующими Flutter и KMP в проде.
Какая сейчас архитектура и инфраструктура вокруг ATHYLPS (тесты, библиотеки и т.п.)?
Сегодня ATHYLPS это KMP-приложение, которое строится на MVI архитектуре, о которой я говорил на Mobius.
У нас почти нет платформенного кода, пока что единственное место, где мы соприкасаемся с платформой — это обработка вытроенных покупок. В качестве библиотеки навигации используется Voyager.
Логика в ядре покрыта unit-тестами, а время генерации упражнений измеряется с в бенчмарках с помощью kotlinx-benchmark. Это бенчмарки на JVM, что может быть не совсем репрезентативно на мобильных устройствах, поэтому сейчас ведется работа по добавлению микробенчмарков под Android. На JVM, к слову, генерация упражнения занимает в среднем 2мс. Я связываю такую производительность с JIT-ованием кода генерации упражнения.
Зачем все-таки меряться временем генерации упражнений в проекте, где это не очень важно? Кажется, что просто отрисовка UI будет медленнее генерации рук?
ATHYLPS это упражнение-тренажер покерных навыков. Концептуально, упражнение выглядит примерно так:
Мы генерируем раздачу на столе, и спрашиваем “Сколько у Вас аутов? И 4 варианта ответов”.
Но помимо набора карт, должно быть сгенерировано еще какое то количество мета-информации о раздаче, которая подается пользователю в качестве обучающей подсказки.
Все наши упражнения работают так. В среднем пользователь генерирует 20-25 упражнений за сессию. Таким образом упражнения — это главный обучающий аспект ATHYLPS, поэтому нам важна производительность их генерации.
Даже текущая производительность генерации (24 мс в среднем) позволяет нам не показывать лоадеры пользователю между вариантами упражнения, что позитивно сказывается на UX. Поэтому появление Core 2.0 никак не связано с производительностью старого решения — ее было более чем достаточно.
Core 2.0 это про корректность и тестируемость, удобство развития и разработки, нативность взаимодействия с клиентом (без JSON). А его кратно лучшая производительность — это просто неожиданный приятный бонус :)
Если вернуться к бизнес-показателям ATHYLPS, то сколько денег он принес? А морального удовлетворения? :)
Говоря про деньги, начну с текущей модели монетизации. В бесплатной ATHYLPS доступны три упражнения. Приобретая PRO версию, пользователь получает доступ еще к трем упражнениям.
На Android PRO-версия — это одноразовая покупка, стоимостью всего $1.5. В лучшие годы, до санкций, Android приносил около $60-90 в месяц. Вычитаем отсюда комиссию Google в 30% (сегодня она 15%). Сегодня доходы Android версии снизились кардинально, так как основная аудитория Android-версии это все таки РФ. В лучшие месяцы приходит по $10.
На iOS PRO-версия — это подписка, стоимостью ~$1.5 в месяц. iOS-версия лучше распространена на Западе, чем Android, поэтому здесь чуть поинтереснее с финансами:
Всего 26 подписок генерируют прибыль на уровне лучших лет Android-версии. Не забываем про комиссии сторов, конечно же.
Мне хочется, чтобы эти скромные показатели не демотивировали читателей заниматься пет-проектами. Ведь посудите сами: эти результаты достигнуты без какого либо маркетинга и четкого понимания как зарабатывать на покерном софте, а мы — просто команда инженеров, которые получают удовольствие от экспериментов с технологиями.
Но это конечно не повод оставлять все как есть. В ATHYLPS 2.0 будет изменена модель монетизации, мы будем адаптироваться к современным реалиям распространения приложений, будет больше ценности для пользователя, которая будет достойна оплаты.
Уверен, что после релиза 2.0 мы сможем вернуться с обновленными (и улучшенными) финансовыми данными, и поделится этим с читателями.
Наконец, как заканчивать и выпускать пет-проекты?
У меня нет универсального рецепта, ведь я забросил далеко не один пет-проект. Моя мотивация, как и мотивация большинства людей — волнообразна. Иногда ты горишь идеей, иногда хочешь все бросить.
Я считаю, что ATHYLPS увидел свет, потому что у команды был понятный бэклог — мы точно знали, что должно быть сделано до релиза. Когда появилось ATHYLPS Core, финиш проекта был вопросом верстки набора экранов. И это уже могло нести ценность для пользователя.
Мы получаем довольно много живого позитивного фидбека от практикующих игроков в покер, и это помогает держаться мотивации на уровне. Кто-то говорит, что идея уникальна и едва ли есть похожие конкуренты, кто-то говорит, что качество приложения довольно высокое по сравнению с другим покерным софтом. Может быть. В чем команда точно уверена — мы еще не смогли толком показать ATHYLPS миру, и с новыми технологиями в ATHYLPS 2.0 у нас появятся все шансы это сделать.