Третий месяц вайб-дебажу настолку в браузере. На что уходит столько времени

Третий месяц вайб-дебажу настолку в браузере. На что уходит столько времени

Веб-версию своей настолки делаю пятый месяц, три из которых уже ушли на вайб-дебагинг. «Release любой ценой» это карточная игра про хаос IT-разработки.

В соцсетях постоянно попадаются посты о том, как за выходные с ИИ собирают игры с 3D и мультиплеером. Инструменты у нас одни и те же, и я не понимаю, почему у них выходные, а у меня месяцы.

Нас в проекте двое плюс модели. Коллега-разработчик держит движок и сеть, я же за то, что видно на экране, и за правила. Прочитать код я могу, а вот понять нет. Разбор пойдёт от того, что я вижу на экране третий месяц подряд, как в дне сурка.

Дизайнерский вгляд на интерфейс не ограничен картинками. По интерфейсу не видно как стол игрока договаривается с хостом или почему у всех одинаковое состояние стола. В таких местах будет вставка со слов коллеги или из описания модели, сам я это пересказывать не возьмусь.

Движения карт

Выбираю карту в руке, она приподнимается и показывает превью. Беру мышкой и вкладываю её на стол, веер за ней смыкается. Отпускаю, и карта садится в центре, а у соперников тоже выкладывается эта карта в том же месте. Одна секунда партии, и я в ней давно не вижу карту, только швы между движениями.

Хотел как за столом

В свою настолку я играл много, и от браузера хотел того же ощущения, что от карт в руках за столом. Решил это ещё в начале, и с первых движений в плейграунде: карта на экране перетекает с места на место вместе с поворотом и размером, без телепортаций, чтобы по анимациям на экране было понятно, что происходит. Стрелку прицела и карту, у которой пульсирует путь, я подсмотрел в больших карточных играх на ПК. И никаких линейных анимаций, они выглядят неестественно.

Не всё со стола стоило нести на экран. Перемешивание колоды я не анимировал, оно бы только затягивало партию. А затяжные игровые моменты за столом получили быстрый розыгрыш на экране.

Что пришлось изобретать для браузера

Готовой библиотеки, которая двигала бы карты в браузере как на столе, не приметил. Карты собирал вёрсткой, и все пути заложил заранее: откуда, куда, по какой кривой, через какие промежуточные состояния, что в это время делают соседние карты. Для этого появился словарь анимаций: десятки модулей, из которых собирается каждое движение на столе.

Что-то вставало сразу: переворот карты, стрелка прицела. А вот над тем, о чем за физическим столом не задумываешься, пришлось посидеть. Разыгранная карта встаёт под случайным углом, чтобы розыгрыши не выглядели одной и той же анимацией. Для сброса отлаживал вид кучей. Отдельная история вход карты в веер. Сначала карта вставлялась по прямой и в конце телепортировалась за соседнюю карту, которая её перекрывает. Потом появилась дуга: карта заходит сбоку, над веером, и на вершине дуги подныривает под него. У крайней карты своё исключение: подныривать ей не под что, и дуга становится прямой.

С соперниками оказалось проще. Соперник на экране это блок, его карты скрыты, а закрытый веер видеть бессмысленно. Карта, которая уходит к нему, улетает в этот блок и пропадает: со стола её забрали, а кто, видно по блоку.

Учесть всё заранее не получилось

Сцены переехали на доску на тех же модулях, и первые партии показали, сколько плейграунд о столе не знал. В плейграунде сцена кончалась посадкой карты. На доске каждому месту, куда карта садится, понадобилось своё состояние, которое игра отслеживает и показывает всем одинаково.

Центр стола. Выкладываю релиз, выбираю карту, которой за него заплатить, а у соперников на экране ничего: будто я ничего не делаю. У меня на экране при этом всё выглядело нормально. Оказалось, стол не знал, что карта уже лежит в центре, и считал её в веере: когда я доигрывал пару, выложенная карта телепортировалась обратно в веер и оттуда ещё раз выходила в центр. Так же двоилось и от хоста, его оповещение «разыграй карту» приходило на карту, которая уже стояла на месте. Решилось правкой движка: в него добавилось отслеживание карт, выложенных в центр, чтобы игра держала состояние стола и показывала всем одно и то же. В движок я лезу редко, и это был тот случай.

Панель состояния хода. Она работала, пока партия не стала попадать в ситуации, которых панель не знала, и эти состояния пришлось дописывать.

Куча сброса. Карты в куче меняли порядок или свои повороты, и сброс пересобирался телепортацией слоёв и позиций.

DDoS по AI-карте. Бью DDoS по AI-релизу в зоне релиза соперника, и карта улетает в сброс, хотя по правилам AI-карта в сброс не уходит и возвращается в колоду событий. По логике игры карта прибавлялась в колоду событий, а на столе при этом лежала ещё и в сбросе. Стол не считал карты и не ловил такие дубли, перепроверок у него прибавилось.

И много чего ещё.

DDoS по AI-релизу и AI-мониторингу соперника на стенде 

Откуда взялся третий месяц

По истории коммитов проект делится на три куска. В июне я собирал стиль и плейграунд: карты, руку, стол, словарь анимаций. Коллега в те же дни ставил монорепо, сеть и лобби. В июле и начале августа появились движок и доска, и 10 августа прошла первая играбельная партия, ещё без анимаций. С середины августа доска получала анимации из плейграунда сцена за сценой: раздача, розыгрыш, добор, стрелка, пара, релиз и защита. С этого момента идёт отладка анимаций, логики и их стыков, чтобы всё собралось бесшовно.

Третий месяц вайб-дебажу настолку в браузере. На что уходит столько времени

Проверка через экран

Отлаживаю я на стенде: та же доска и тот же движок, только состояние партии собирается из пресета, и одну сцену можно гонять сколько угодно. Разыгрываю карту, вижу, что не так, описываю модели, она правит, я смотрю снова. Долго я описывал ей увиденное словами. Потом сообразил, что анимации интерфейса можно писать в лог: скинул лог, и модель увидела те же кадры, что я глазами, а под длинные логи сама сделала анализатор, который называет проблемные места.

Проверка есть и до правки: по ответу модели видно, куда она полезет, в модуль или заплаткой в сцену, и держится ли она правил игры. И после: каждый PR проверяют модели, потом смотрим сами, я со стороны экрана и правил, коллега со стороны кода.

Третий месяц вайб-дебажу настолку в браузере. На что уходит столько времени

Сижу я в долгих сессиях в Claude Code, и если модели по задаче нужны агенты, она сама их и запускает. По нынешним меркам вайбкодер из меня так себе: оркестра из агентов я себе не собирал. Задачи, которые себе ставлю, редко выглядят как спека с понятным итогом. Обычно это продуктовая задача, что должно происходить за столом, а до нюансов докапываюсь уже по ходу правок. Даже по точному описанию модель может принести не то. Если нет видения конечного результата, то и нет больших кусков, чтобы отдавать группе агентов. Каждая задача получается как эксперимент, который я проверяю через экран.

Правки волнами

Основной ракурс доработок идет через отладку модулей анимаций. Каждое движение на доске собирается из словаря, и правка идёт в модуль (по задумке), даже если дефект нашёлся в одной сцене: поправил модуль, и вместе с ним проверяю все сцены, которые им пользуются. Модель плохо удерживает широкую работу с модулями: пишет движение внутри сцены, в обход модулей, и правит движение ситуативно. Такая точечная правка работает только внутри своей ситуации, тогда в другом месте то же движение остаётся с тем же дефектом. Поэтому тему я не закрываю одним заходом: правлю всё, что правка задевает, и отлаживаю волной целиком, вместе с переводом частных копий на общее. Отслеживать, в верном ли ракурсе она правит, я научился не сразу, и до сих пор есть упущенные места.

Третий месяц вайб-дебажу настолку в браузере. На что уходит столько времени

Одна карта по кругу

Cherry-pick берёт любую карту из всего сброса. В одном розыгрыше много движений: карту выкладывают в центр, куча сброса раскладывается сеткой на весь стол, выбранная карта летит в руку, остальные возвращаются в сброс, а с Sudo вторая карта уходит наверх колоды добора так, чтобы соперники её не увидели.

К этой карте я возвращаюсь снова и снова.

  1. Первый раз правил её со стороны игрока: карта выкладывалась в центр, вызывала сетку выбора и уходила наверх кучи, и всё это лагало и телепортировалось. Выправил на глаз всё, что видел, и картинка стала ровной.
  2. Потом, уже на других картах, заметил, что правил я тогда картинку, а модули не трогал. Пересобрал розыгрыш заново на модулях (так я думал, наивный…): выход в центр, сетку, уход в сброс.
  3. Следующие карты показали, что и этот заход прошёл мимо модулей, и розыгрыш пересобрал ещё раз.
  4. После ревью коллеги и разбора его комментария модель научилась видеть модули анимаций, и правка одного места стала чинить соседние сценарии сразу, Cherry-pick в том числе. Заодно выяснилось, что модулям самим нужна доработка, и этот заход был уже по всем анимациям через модули.
  5. Потом отдельный заход на выбор карты из сброса: добавил и чинил просвечивающий оверлей, карты из сброса приходили в сетку из пустоты, а не из сброса, в кучу сначала возвращались и только потом перестраивались. Затемнение стало одной поверхностью на все карты с выбором, карты вылетают из кучи со своих мест и возвращаются на них в новом порядке, а выбор карты релиза от AI Inside сел на ту же реализацию.
  6. Последним Cherry-pick зацепил заход с центром стола, тот, после которого игра отслеживает карту, выложенную в ожидании пары. Cherry-pick играется в паре с Sudo, и пришлось пройти по нему ещё раз: пара перестала меняться местами после посадки.

Сейчас я иду по всем анимациям заново и уже вижу места, написанные мимо общего словаря. Значит, Cherry-pick буду проверять снова.

Cherry-pick с Sudo на стенде 

Прерывания внутри хода

«Release любой ценой» построена на прерываниях внутри хода. Выложил релиз, и открылось время атаки: каждый соперник может вбросить багов. Когда одну атаку отбили, открывается окно для новой атаки релиза. Sudo ждёт выбор карты, которую будет усиливать. Code Review не даёт атаковать свежий релиз. Error 503 срабатывает посреди добора и может выбить игрока из игры. AI-карта разыгрывается немедленно, прямо во время добора, и открыта всем. За реальным столом порядок всего этого задаёт живая очередь: люди видят друг друга, и всё происходит по наитию.

В коде так не получится. Кто сейчас может действовать, сколько у него времени, что видят остальные и что будет, если он не успеет, решено заранее, до того, как момент наступил. Всё должно работать одинаково для всех игроков.

Все эти решения игрок видит в панели состояния хода, под колодами добора. Панель показывает, что происходит в игре прямо сейчас, а кнопка действия отображается, только когда нужна. В игре, где ход идёт по прямой, хватило бы одной кнопки с заблокированным состоянием до следующего нажатия. Здесь панель подстраивается под игровую ситуацию, и держать клавишу на экране просто так нельзя: у соперников на её месте имя того, кто ходит, а у игрока, сделавшего релиз, количество спасовавших игроков.

Панель состояния хода в плейграунде 
Панель состояния хода в плейграунде 

Отдельное решение понадобилось для паса. Пас значит, что я не против, чтобы время атаки закончилось раньше. Что это не отказ от атаки, я решил в документации, до реализации. На экране же пас сначала жил только в логике: спасовал, движок запомнил, на столе ничего не изменилось. Потом у владельца релиза появились точки по числу соперников, и пас стал отметкой, которая игрока ни к чему не обязывает. Спасовал, и у владельца добавилась точка, без имени: он видит, сколько спасовало, а не кто. Передумал, и снимать пас не обязательно, можно просто кинуть атаку. А когда спасовали все, время атаки заканчивается досрочно.

Таймеры на действия предложил коллега: за живым столом времени нет, а в онлайне без него партия может остановиться, если кто-то отвлечётся. Таймеры выставили по своим тестовым партиям и тестировали, как они ложатся на игру. Считает таймер ожидание действия, а не ход целиком: разыграл карту в свой ход, и отсчёт пошёл заново, так что пока игрок действует, время его не поджимает. На мне же и проверилось: отвлёкся посреди живой партии, время вышло, и игра завершила мой ход. Строил таймеры из позиции: игра не разыгрывает карты за игрока. Вышло время, и она доводит начатое: завершает ход, засчитывает атаку, которую не отбили, возвращает в руку релиз, за который не заплатили. Там, где без выбора не обойтись, выбор случайный.

Колода событий

Игра может прерывать даже добор. Самая многочисленная карта основной колоды это AI: вытянул её, и она открывается всем, а следом игра тянет случайную карту из колоды событий и тут же её разыгрывает. Что вытянется, игрок не выбирает, в этом и хаос. Good Vibe-Coding добирает ещё две карты, и среди них может оказаться следующий AI. Bad Vibe-Coding заставляет сбросить карту. Hallucination обрывает ход прямо посреди добора, и больше ни одна карта в игре так не делает. За живым столом это пауза: отложить добор, взять вторую колоду, показать, что взял, разыграть. На экране это быстрый розыгрыш: AI-карта лежит в центре рядом с событием, пока оно разыгрывается, потом событие возвращается в колоду событий, из игры эти карты не уходят. Придумывал я их про вайбкодинг до того, как сам стал отлаживать Hallucination от моделей.

AI-карта при доборе, Hallucination обрывает ход и возвращается в колоду событий

Уже играбельно

В базовом режиме игра уже стабильна и ей можно делиться. Мера готовности у меня одна: игра сама держит правила, и карты на столе движутся без видимых швов. Картонным картам хватает стола и гостей, остальное происходит само. В веб-версии это всё пришлось делать.

Игра держит правила и связь

Веб-версию я делал так, чтобы знакомые могли сесть играть и не выясняли, как трактовать правила. С любой новой настолкой первые партии уходят на то, чтобы в ней разобраться: правила приходится запоминать на ходу, что-то читается не так, кто-то продавливает свою трактовку, и в итоге играешь не в ту игру. Здесь правила контролирует сама игра: карта, которую сейчас играть нельзя, на стол не ляжет. Подсветка в руке появляется только тогда, когда игра ждёт выбора, например чем заплатить за выложенный релиз. Хост поверх этого проверяет каждое действие ещё раз и следит, чтобы у всех было одинаковое состояние игры. За живым столом всё это ложится на игроков.

Игрока, который пропал со стола, игра не теряет. Место остаётся за ним: обновил вкладку или открыл ссылку заново, и он продолжает игру.

Перезагрузка вкладки посреди партии и возврат за стол 

Выбывший становится зрителем

Выбывший игрок остаётся за столом до конца партии и видит её так же, как зритель. Роль зрителя в игре выросла из взгляда выбывшего после Error 503. Зрители не видят карт игроков, им доступен вид стола, как если бы все игроки были для них соперниками. Игроки и зрители сидят в одном и том же текстовом и голосовом чате.

Один и тот же момент партии глазами игрока и глазами зрителя 

Зрителем садится тот, кому не досталось места игрока, или кто пришёл по ссылке в уже идущую партию. После партии он остаётся в комнате на следующую. Сколько зрителей пускать, решает хост.

Сбор игроков

Для входа в комнату нужен ник, а чтобы не придумывать, есть кубик со случайными вариантами. Аватарку игра выдаёт сама, поменять её и ник можно в лобби. Играть или смотреть человек отмечает на входе, а решает хост: пересаживает между игроками и зрителями только он.

Чат я в игру не закладывал: созвониться можно где угодно, зачем он ещё и в игре. Коллеге, с которым мы вдвоём гоняли первые партии, созваниваться отдельно от них не нравилось, так в комнате появился текстовый чат. Голосовой добавился, когда коллега напомнил, что его сервер в голос уже умеет и нужен только интерфейс.

Bug Runner 

Ждать остальных в лобби знакомым было скучно, они так и сказали. Теперь в шапке сидит Баг, пиксельный раннер вроде динозаврика в браузере без сети. Нажал, и он побежал: перепрыгивает хотфиксы, пригибается под мониторами и кушает серверы. Чем дальше, тем быстрее, мониторы начинают менять свою дорожку, навстречу выбегает резиновая уточка, а подобранная карта Sudo дает дополнительную жизнь. Хост помнит рекорд забега, и в итогах партии за него можно получить ачивку.

Сыграть можно и одному: хост добавляет в комнату ботов, до пяти. Ходят они по тем же правилам, что и люди, и по ощущениям играют жёстко: атакуют всем, чем могут, как только могут.

Конец игры

Партия заканчивается конфетти, которое видит только победитель, и все переходят к статистике. Её формат я подсмотрел в существующих играх, а материал для неё у игры уже был: история ходов собирается для отладки, и её видно во время партии. Раз игра помнит каждый ход, значит может их посчитать, значит статистику можно превратить в ачивки. За живым столом таких подсчётов никто не ведёт, а тут само все посчитается. Самым сложным оказалось придумать ачивки в вайбе игры.

Ачивки
Ачивки

Комната живёт дольше одной партии. Доиграли, нажали «в лобби», и та же компания с теми же ботами сидит в комнате у хоста, отметились готовыми, и играете следующую катку. Так же подряд идут и тестовые партии: всё в одной вкладке, голосовой чат не прерывается, а текстовый держит историю всех партий.

Обратно в картон

Веб-версия повлияла на правила картонной игры. В коробке партия начиналась с одной колоды добора, и добирали всегда одну карту, даже если колод на столе лежало несколько. Онлайн-партии показали, что с двумя колодами на старте и добором по карте из каждой играется динамичнее: рука наполняется быстрее, и в свой ход почти всегда есть чем действовать. В веб-версии за это отвечают два режима, добавленные уже по ходу работы над игрой, добор из всех колод и число колод на старте, и такой набор теперь стоит базовым. Карты при этом не изменились, доработан только текст правил, и с ним тираж готов к печати, но пока не напечатан.

Увидеть это помогли тесты: при отладке их стало уж слишком много, потому что партию больше не нужно раздавать каждый раз. Нажал кнопку, и всё готово, боты уже ждут за столом.

Два режима, добавленные в процессе работы над игрой 
Два режима, добавленные в процессе работы над игрой 

Так куда уходит время

Всё, на что ушли эти месяцы, в готовой партии не видно. Я дизайнер, и мерой хорошего интерфейса считаю то, что его не замечают, так что в каком-то смысле получил, что хотел. Как карта приходит в веер, как куча сброса ложится одинаково хаотично для всех, как розыгрыши разных карт с руки идут без швов, как обновлённая вкладка возвращает за стол, и за столом никто ничего не заметил. Заметно становится, только когда что-то ломается. Третий месяц уходит на нюансы, которые в игре незаметны и воспринимаются «ну так и должно быть», пока работают.

  • Анимации карт. Планка одна: как за столом, карта перетекает с места на место и никогда не телепортируется. Время ушло на разницу между сценой в плейграунде и доской: каждое движение я собирал в словаре, смотрел, как оно живёт на доске, и переделывал, пока они не совпали. Кругов по каждому движению было столько, что всех я уже не упомню.
  • Проверка через экран. Расхождения я вижу только через интерфейс, поэтому каждую правку проверяю с ботами или на стенде. Правка цепляет сразу несколько сценариев, и разыграть нужно каждый.
  • Правки волнами. Отдельная статья времени это борьба за то, чтобы игра на самом деле пользовалась словарём модулей анимаций. Модели проще починить движение внутри сцены, и мне приходится по каждой правке следить, куда она легла, в модуль или в сцену, и возвращать её в модуль. Так дизайнер, который не умеет в код, воюет за его чистоту.
  • Корректность правил и отладка прерываний. Окно атаки, пас, таймеры, карты событий. Время здесь ушло на вопросы, которых правила на бумаге не задавали, потому что за столом их решала живая очередь: кто сейчас может действовать, сколько у него времени, что видят остальные. Каждый ответ ложился сначала в спеку правил, потом в панель состояния хода, потом на доску, где движок и экран расходились.
  • Окружение партии. Что за столом понятно без слов, кто играет, кто смотрит, кого сейчас нет, здесь каждому нужно показать на экране. Экранов у комнаты вышло больше, чем у самой игры: стол один, а вокруг него лобби, приглашение и итоги, и все с единым чатом.

Самое дорогое из этого движение карт без швов. После него доработки остального интерфейса идут легко, и по времени, и по ощущению. Игровые анимации надо было с самого начала делать отдельной библиотекой, со своими тестами. Поняли мы это с коллегой только недавно.

Ответ на вопрос из начала у меня получился без открытий. Время уходит на то, что и до правок делало своё дело, просто хуже на вид. Это внимание к мелочам, которое у опытных дизайнеров считается нормой и которое, возможно, никому кроме меня не нужно. Хотя один человек разницу замечает: коллега видит стол до и после моих PR и говорит, что стало лучше, а на первых тестовых партиях сам называл, какие движения уже отлажены, а какие ещё нет, и однажды сказал «анимация как в плейграунде».

А с роликами за выходные я сравнивал не то. По ним видно, что модель умеет сегодня и что собранная за выходные игра работает, в неё можно сыграть. Что происходит после «можно сыграть», в роликах не видно. Сжалось бы оно у меня с агентами до выходных, не знаю. У нас играбельная партия случилась 10 августа, а с тех пор я отлаживаю то, что в игре обычно незаметно.

Третий месяц вайб-дебажу настолку в браузере. На что уходит столько времени

Третий месяц продолжается

Будем отлаживать дальше, хочется добиться полной стабильности. В базовом режиме уже играется хорошо, но на других режимах анимации я пока не проверял. Ещё игровые анимации надо вынести в самостоятельную библиотеку. И остаётся незаконченной сеть.

Сеть уже почти P2P: когда все собрались, партия идёт между браузерами напрямую, звездой через хоста. Сервер коллеги знакомит браузеры друг с другом и проводит через себя тех, кому напрямую не достучаться, а также держит голосовой чат. Полноценный P2P, каким его задумали изначально, остаётся целью.

Сыграть можно сейчас: https://mythhand.github.io/ReleaseBoardGameP2P. Что сломалось и что понравилось, пишите в комментариях или в группе в телеграмме, обе стороны мне одинаково интересны.

Код лежит здесь: https://github.com/MythHand/ReleaseBoardGameP2P, баг можно занести сразу в issue.

Полная версия этой же статьи опубликована на Хабре.