Два месяца назад мы взялись перенести в веб мою настольную игру. «Release любой ценой» — карточная игра про хаос IT‑разработки: собираешь релиз из Backend, Frontend и Database, соперники его валят, ты отбиваешься. Про саму настолку я здесь уже писал.
Нас в проекте трое: разработчик (ditayler), я (dimbo‑design, дизайнер) и Claude Code, через которого работаем оба. Ему досталась реализация логики игры и фундамент: монорепо, сеть, движок. Мне видимая часть: интерфейс, анимации, поведение карт, UX. Прочитать код я, в общем‑то, могу, а вот понять нет: моё ревью проходит на экране, как дизайн‑ревью.
Из этой конфигурации торчит неудобный вопрос. С моей стороны в проект постоянно едет код. Как убедиться, что туда не едет мусор, если прочитать его я не в состоянии? Ответ, на котором стоит проект: нестыковки должны всплывать сами — между договорённостью и кодом, между докой и реальностью. И сразу честно, чтобы не выглядело героическим строительством по ходу: большую часть этого фундамента разработчик задал ещё на старте, и именно фундамент работает. Что‑то доросло позже в моей зоне, за что‑то пришлось повоевать. Дальше эта система по частям, в порядке появления в проекте.
Пощупать текущее состояние можно тут:
Приложение на Pages — https://mythhand.github.io/ReleaseBoardGameP2P
Плейграунд — https://mythhand.github.io/ReleaseBoardGameP2P/playground/welcome
Репозиторий — https://github.com/MythHand/ReleaseBoardGameP2P

Поделили поле
В первый день зоны разошлись по владению. Ему фундамент. Мне плейграунд: песочница визуального слоя, где живут карты, рука и стол, и где я могу крутить своё, не задевая чужих файлов. Первые коммиты проекта так и выглядят: порт визуального слоя с зачатком словаря анимаций и рядом скелет монорепо. За первые же дни возле скелета встали рельсы: общие конфиги линтеров и TypeScript на весь воркспейс, CI с проверками на каждый PR, пре‑коммит хуки. Видимого интерфейсного прогресса ещё не было, а рельсы уже стояли. Дальше части развивались независимо: фундамент своим ходом, интерфейс своим.
Ещё до экранов сформировал текстовый док: что это за игра, из чего состоит карта, какие в ней интерактивные моменты, чего мы точно не делаем. Оттуда, например, принцип «экран — это экран»: никакого деревянного стола, фетра и скевоморфизма, только плоские карты на тёмной экранной поверхности. Это ещё не система, но роль у документа та же: не дать уйти от изначальной задумки.
Договорённости кладём в репозиторий
Два независимых трека держатся вместе только на общей памяти. А с ней бытовая проблема: если не зафиксировать договорённость, о ней забудут. Её не помню я, не помнит коллега, и точно не помнит модель, у которой каждый разговор первый. Поэтому договорённости лежат в самом репозитории, так заведено с начала. Своды правил для Claude у корня и у каждой зоны пропущу: вещь по нынешним временам стандартная. Решения покрупнее оформляются датированными спеками, их сейчас двадцать три: восемь про стол и его анимации, пять про экраны, три про каркас и инфраструктуру, три про систему стилей, две про сеть и по одной про игровой цикл и работу с гитом. Подход задан с первого дня, причём раньше самого кода: сначала спека реструктуризации монорепо и план реализации к ней, потом скелет. Пара «дизайн‑спека + план» с тех пор повторяется по проекту: так оформлялись сигналинг‑сервер, лобби, интерактив стола.
Самая первая пара: спека реструктуризации монорепо и план к ней. Скелет монорепо появился следующим.
Нестыковка «а мы разве так договаривались?» перестаёт быть спором двух памятей: есть точка истины. Но это всё ещё слова. Работают они на того, кто их прочитал, а читать документацию не любит никто.
Запреты отдаём линтеру
Часть правил живёт там, где их вообще никто не обязан помнить. Захардкоженный цвет в стилях запрещён сводом правил: цвета только токенами. В файле токенов 33 именованных цвета: поверхности «экрана», восемь категорий карт из настолки, акценты состояний. Рядом 15 полупрозрачных вариантов тех же цветов, 55 ступеней белого и чёрного под оверлеи и 5 градиентов. Всего в файле 133 переменные, остальное типографика, геометрия карты, слои интерфейса и тайминги. Текст, вписанный в разметку мимо каталога переводов, не собирается: ключ обязан существовать и в русском каталоге, и в английском. Рукописный font‑size под тем же запретом: весь текст идёт через один типографический компонент с общей шкалой, а в шкале 40 именованных текстовых стилей и 15 шагов трекинга.

Для продуктового веб‑проекта три десятка именованных цветов и четыре десятка текстовых стилей — много, обычно скромнее. Но в геймдеве элемент нередко просто рисуется картинкой, а у нас свечение категорий, поверхности, оверлеи и надписи на картах собраны кодом. Это цена того, что игра выглядит игрой, оставаясь вёрсткой.

Цвет, вписанный руками, я в диффе ещё замечу. Логику, грамотность построения вёрстки, последствия решений в коде — нет. Поэтому всё, что можно отдать машинной проверке, отдано ей: правило, которое соблюдается само, не требует, чтобы я умел его проверять.
Tailwind спор не пережил
Не всё заезжало мирно. В первые недели во фронтенде появился Tailwind: коллега не стал ждать, пока у меня появится своя система, взял готовую и показывал, как мои стили на неё ложатся. Там, где он её уже поставил, дизайн ломался сразу: отшлифованные нюансы уезжали, и это было видно. Я говорил, что на сложных экранах будет только хуже, а системность на перспективу, ради которой Tailwind обычно и берут, конечному проекту не нужна. Спор кончился тем, что я отстоял полностью кастомные стили, и Tailwind первым вылетел из продакшен‑сборки.
Систему элементов вместо него я формулировал, согласовывая с коллегой, и её правила ушли в тот самый линтер из прошлой секции. В репозитории какое‑то время лежал файл NO_TAILWIND, на память. Потом выпилили и его, хотя сессии коллеги, говорит, иногда ещё вспоминают, что Tailwind сюда нельзя.
КЕЙС / С него началась своя система
До этого система жила у меня в плейграунде, а в проекте стояла чужая. К июлю один и тот же экран был написан дважды: в плейграунде — на CSS‑модулях с токенами, во фронтенде — на утилитах Tailwind. Копии правились порознь, и песочница с приложением показывали разное. В копии фронтенда цвета стояли числами:
#8fd9b0и#ff6b81вместо--mintи--coral, так что менять токен было бесполезно, до этих мест правка не доходила.Убрать одну копию значило выбрать, которая из двух систем определяет экран. Выбрали мою: Tailwind вылетел, источником в спеке назначили экран из плейграунда, обратно он не заедет —
@themeи@utilityдля stylelint незнакомые at‑rule’ы, сборка на них падает.Свою я собирал от того, что нужно проекту: под каждое место можно завести свой стиль. Позже пробовал сократить и унифицировать — не понравилось. Получался очередной сервис, где всё собрано шаблонными стилями. Унификацию отбросил, самобытность оставил — ту, что была в сайтах раньше. На ней собрано ощущение от игры.
Словарь анимаций
По анимациям я с самого начала целился в бесшовность: чтобы все движения карт на столе были отлажены и перетекали друг в друга — попытка дотянуться до уровня полноценных ПК‑карточных игр.
Идея модульности у меня была с первого дня: словарь модулей лежит ещё с первых коммитов. Дальше случилось поучительное. ИИ модульность охотно декларировал, а на деле не использовал: движения писались мимо словаря. Заметил я это не по коду, а по нестыковкам в ответах: рассказы модели о том, как всё устроено, перестали сходиться между собой. Дальше вопрос в лоб, анализ кода и перестройка. В начале августа весь слой отрефакторен на общие элементы и шаги, модульность стала настоящей. В разговоре с коллегой, кстати, выяснилось, что мой подход к анимациям он считает грамотнее того, как собирался работать с ними сам. Приятно, когда твою часть системы признаёт тот, кто задал остальную.

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

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

Спека анимаций собиралась отдельно и сразу под формат для ИИ: справочник модулей, рецепты игровых ситуаций, глоссарий. Это в первую очередь материал, по которому работает модель. Заявлениям «всё описано и всё сходится» я после истории с модульностью на слово не верю. Тест здесь ловит каркас: модуль без строки в справочнике роняет сборку. Содержательно спеку актуализирую сам, отдельными заходами — и это самое слабое место всей конструкции: состав держит сборка, а за верность написанного отвечаю только я. У этой дисциплины есть и побочный эффект: вольные эксперименты с анимациями стали дороже — теперь, дорабатывая движение, модель сама требует завести его в модуль по уму.
КЕЙС / Справочник сначала был фиктивным
Первая версия справочника выглядела образцово: каждый модуль описан, у движений параметры, всё сходится. Я ему верил, модель по нему работала. При этом местами описания расходились с кодом: справочник рассказывал, каким словарь задуман, а не каким он был.
Враньё вскрылось в августе, на сплошной сверке доки с кодом. Часть описаний врала. Вход карты в веер по справочнику всегда открывал щель в середине, а в коде входа два: добор идёт в середину, потому что у карты с колоды нет своего места в руке, а перетаскивание идёт в слот, который указал игрок, — этот путь я отлаживал отдельно. Рука, собранная по справочнику, вставляла бы карту в центр и игнорировала указанное место. Других модулей в справочнике не было вовсе. Превью карты стояло на странице аудита готовым, им пользовались четыре сцены, в справочнике о нём ни строки. Ни первое, ни второе не шумит: ничего не ломается, расхождение просто лежит. Пропущенный модуль ещё и удваивается: модель читает доку вместо исходников и пишет движение заново, рядом с настоящим.
Проверок после этого стало три: справочник против реестра модулей, справочник против страницы аудита, рецепты против списка сцен. Но каждая сравнивает два списка между собой, а модуль, не попавший ни в один, невидим для всех трёх. Эта дыра записана в бэклоге, и тестом её не закрыть — держится она правилом: модуль считается сделанным, когда появился на странице аудита.

Отступление про карты
У игры есть телеграм‑стикерпак — из иллюстраций и некоторых карт, адаптированных под стикеры, — и в рабочем чате он иногда оказывается точнее слов. Коллега пишет, что подлагивает анимация (оказалось, в Сафари). Я не сразу понимаю, о чём он, и в ответ скидываю запись, где у меня на Хроме всё хорошо. На такие сюжеты в чате находится стикер по карте «Works on my Machine». Ещё летают «Bug», когда нашлась нестыковка, и «PR Approve», когда я апрувлю PR коллеги после разбора от ИИ. Карты добирались даже до бэклога спеки: запись о рассинхроне правил и кода так и называлась — «Works on my Machine». Колода про хаос IT‑разработки отразилась в своём же проекте. Собственно, юмор игры так и устроен: прямых шуток в ней нет — карты создают рабочие ситуации, а флешбеки каждый подставляет свои.
Спека правил, которой запрещено догадываться
Правила игры — тоже моя зона, и с ними я наступил на те же грабли, что со словарём анимаций. Думал, хватит md‑файла с их текстом: модель умная, разберётся. Не хватило. Понадобился формат, понятный модели, и переводить в него помогала она сама: гоняла меня вопросами «а что это значит», «а что случится, если», пока картина не стала однозначной. Так правила стали технической спекой: каждая карта с id, эффектом и тем, что её отменяет, порядок разрешения, режимы.
КЕЙС / Спека написана так, чтобы не читалась иначе
Технической спеку делает способ записи. Правило пишется на идентификатор карты: «Security Bug» — подпись на арте,
attack-security-bug— то, что ищется в коде. Тираж карты и лимиты руками не переписываются, они берутся из каталога. Формулировки буквальные: «окно атаки» пришлось развернуть в «время, когда соперники могут атаковать свежий релиз, а владелец от этих атак защищаться», потому что короткое имя каждый разворачивает в то, что и так думал.И одинаковые слова разведены: «защита» в тексте правил — карты, которые стоят в зоне против триггеров, а в коде
defense— карты, отменяющие чужую атаку в ответ; в спеке под это заведена таблица, иначе читающий текст и читающий код разойдутся, не заметив.Зачем эта дотошность, видно, если её убрать: по спеке пишут код, закрепляют тестом и дальше сверяются с ней как с источником, так что неточная формулировка становится движком, который играет в другую игру.
У спеки было жёсткое правило: догадке нельзя становиться правилом. Там, где текст правил допускал два прочтения, спека не выбирала удобное: место помечалось маркером «❓ Не из правил», а в бэклог ложились оба прочтения и чем грозит неверный выбор. Решал я как владелец правил, ответ уезжал в спеку, запись закрывалась. Я было думал, что на этом вопросы кончились, но работа продолжает поднимать новые. Пример из бэклога: игрок разыграл карту релиза, но ещё не оплатил её цену — видят ли соперники стоящий на столе релиз, или стол узнаёт о нём только после оплаты? В печатных правилах про этот промежуток ни слова. Ответил я так: видно всё, что выложено на стол, и неоплаченный релиз соперники видят с момента розыгрыша. Ответ уехал в спеку на место метки. За движок говорить не возьмусь, он делается на той стороне границы; моё дело — чтобы спека, по которой он делается, не врала.

Эта спека — граница наших зон: с одной стороны я, с другой движок, который по ней делается. Как игра должна работать, очевидно и однозначно для всех: для меня, для коллеги, для модели. А где неоднозначно — это видно по метке заранее, а не выясняется после реализации.
Как половины подгонялись друг под друга
Зоны, которые мы в первый день развели, перемешивались по договорённостям, а не бардаком. Системность в мою половину принёс разработчик: тот ранний спор за стили был ровно об этом — он взял чужую готовую систему, чтобы не ждать моей. Признаюсь, сначала я на системность благополучно забил. Дошло, когда увидел чужую систему в проекте: чтобы было по‑настоящему хорошо, своё надо собирать полноценно, не как идею, а как систему. Пришлось догонять. Догнал и перегнал, наплодив примитивов: пересобрал и переотладил сделанные к тому моменту экраны, благо их было ещё немного, а идею развил дальше до блоков, по образу готовых компонентов в Фигме.
Перестройки на этом не закончились. Обычно их запускали доработки его фундаментальной части: я просил модель проверить, всё ли моё туда ляжет, находились нестыковки, дорабатывал. Бывало, подмечал общий момент, и каскадом выходили широкие правки почти всего. До «сломалось» не доходило: выручали опыт и перестраховка в решениях.
Шаги навстречу были и с его стороны. Живой интерфейс сам работал аргументом: когда коллега видел на экране, как что должно работать, потенциальные вопросы «а нужно ли это» снимались до того, как прозвучать. Он же показывал коллеге, где фундамент ещё недотягивает, чтобы снабжать интерфейс достаточным количеством данных.
Склейка: что происходит сейчас
Части срастаются в игру, и у этой работы есть письменный план: каждая интерактивная хореография из плейграунда должна доехать до настоящей доски. Плейграунд в плане назван утверждённым визуальным источником: сцена там — определение, а доска учится играть её от событий движка, а не от демо‑кнопки. Работа идёт волнами, каждая опирается на предыдущие: сначала доска вообще научилась анимировать и сыграла первую карту, потом добор и колода, стрелка прицела, комбо‑пара. Пока я дописываю этот текст, коллега смержил самую большую сцену: релиз и защита на доске, ядро игрового цикла.

Моя роль в этой фазе — не мешать. Фронт впитывает решения из плейграунда, и пока это идёт, я сознательно не трогаю свою половину; находки от склейки стекаются в аудит, а вопросы расходятся по бэклогам — свой у спек, свой у правил, свой у анимаций; доводка пойдёт следом, уже вдвоём.
Не мешать не значит выпасть: каждый его PR я отдаю на разбор своей модели. У наших сессий разные ракурсы на продукт, поэтому нестыковок с задумкой находится достаточно, и они остаются комментариями в PR. Исходный вопрос статьи здесь разворачивается: фундамент, заданный разработчиком, вместе с частями, доросшими в моей зоне, показывает нестыковки не лично мне, а всем участникам, включая модель.
КЕЙС / Находки не существовало
В реестре находок появилась запись: во время оплаты релиза превью карты из веера встаёт над центром стола; у превью
pointer-events: none, поэтому нажатие проваливается сквозь него, не находит слота руки, и слушатель на корне стола читает его как промах и отменяет розыгрыш. Грозило это потерей хода без единого признака ошибки, там, где игрок изучает руку: навёл карту прочитать, нажал — релиз уехал домой, а связать отмену со своим движением он не может: он нажал на карту. Тесты такое не ловят: jsdom не считаетpointer-events, и нажатие в тесте попадает туда, куда его послали.Мой ответ: события не существует — превью держится наведением мыши и уходит вместе с ней, поэтому нельзя одновременно держать превью и нажать в закрытую им область. Запись закрыта со статусом «не воспроизводится по природе жеста» и осталась в реестре вместе с разбором. Разбор модели заходит в реестр наравне с моими, но существует ли событие, решает владелец. Закрытая запись стоит там же, где открытые, поэтому второй раз этот вопрос никто не поднимет.
Что держит проект: сводка
Соберу систему в один список, с адресами в репозитории:
своды правил для Claude (CLAUDE.md в корне и в каждой зоне: apps/ui, apps/playground, apps/frontend, packages/lint): общее лежит в репозитории, сессионные правила у нас с коллегой свои;
датированные спеки, у крупных рядом план реализации (docs/specs, пары «design + plan»): решения записаны до кода;
линтеры с запретами: собственный плагин к Biome не пропускает строки мимо каталога переводов (packages/translation, ключ обязан существовать в ru и en), stylelint держит логические свойства и роняет незнакомые at‑rule’ы; цвета только токенами (apps/ui/src/design/tokens.css) и весь текст через компонент Typography — это запреты свода правил, а не сборки;
пре‑коммит хуки и CI (simple‑git‑hooks с lint‑staged,.github/workflows): на коммите линтеры по изменённым файлам и типы по всему воркспейсу, на каждый PR отдельными задачами типы, тесты, сборка и линт;
плейграунд как утверждённый визуальный источник (apps/playground): сцена там — определение, доска играет её от событий движка; при расхождении правится то, что отошло от сцены — по этому правилу идёт перенос хореографий на доску;
документация анимаций под ИИ (docs/animations: справочник, рецепты, глоссарий); состав сверяет тест docs.test.ts, содержание актуализирую сам отдельными заходами;
страница аудита (Interaction audit в плейграунде): статусы модулей, реестр находок и тест, проверяющий, какие сцены реально доехали до доски;
спека правил (docs/rules: каждая карта с id, порядок разрешения, режимы) с механизмом «Не из правил» и бэклогом;
бэклог при каждой спеке (docs/rules/backlog.md, docs/animations/backlog.md, файлы открытых вопросов в docs/specs): сама спека описывает только то, что решено, нерешённое уходит в бэклог по фиксированной форме — что непонятно, чем грозит, где помечено, статус. У анимаций запись двойная: строка в реестре находок на странице аудита, чтобы её видели, и полный разбор в бэклоге, чтобы её закрыли;
на той стороне границы — движок (packages/engine) со своими тестами; он ещё делается.

Что ещё не готово
На странице аудита копится буфер находок от склейки, его ещё разбирать. Там же лежит баг руки: при доборе на большой руке карта встаёт не в то место веера. Формула веера тут ни при чём, её тест зелёный, — где именно теряется место, ещё не разобрано, и баг отложен осознанно. В бэклоги уходит то, что склейка вскрывает по ходу: места, которых плейграунд не предусмотрел, и движения, под которые нет модулей. Заводить недостающее я сяду, когда перенос закончится.
Релиз, как положено в этой игре, ещё не собран, партия в разгаре. Когда соберём, карта «PR Approve» уйдёт в чат по назначению.
Репозиторий проекта — https://github.com/MythHand/ReleaseBoardGameP2P
А если интересно дождаться, во что это выльется, за проектом можно следить в моём инстаграме: про выход в бету или продакшн там сделаю отдельный пост.