Site icon AppTractor

ATHYLPS: пет-проект, приносящий радость от экспериментов с технологиями

Снова говорим с Сергеем Опиваловым, 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 тогда был довольно экспериментальным, но мы знали что он активно используется в некоторых проектах, типа Яндекс Карт, что добавляло оптимизма, и я решил взять его для реализации прототипа.

Нужно сказать о том, что должно делать ATHYLPS ядро:

  1. Декларировать покерные примитивы — карта, масть, ранк, колода, набор комбинаций и другое
  2. Оценка покерных рук. На вход принимается массив карт, на выходе — комбинация
  3. Сравнение рук. Две руки можно сравнить и сказать какая из них сильнее (или ничья)
  4. Доставать “значимые” карты из любой руки
  5. Генерировать неограниченное количество вариантов упражнений, основанных, по сути, на генерации рандомных рук и их эвалюации

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

Мне не удавалось ее как либо адекватно решить, и я начал искать решения на 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 и его поддержке в течении жизни приложения. Фиксы в ядро всегда были не самой приятной вещью и требовали набора церемоний. Вот примерный список проблем, которые можно выделить:

Мы долгое время не пытались мигрировать ядро на 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 на не идиоматичный 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 у нас появятся все шансы это сделать.

Exit mobile version