VanguardReMaster/docs/HANDOFF.md

33 KiB
Raw Blame History

Передача сессии

Состояние проекта на 21 октября 2026. Документ нужен, чтобы новая сессия продолжила работу без повторного разбора истории.


1. Проект

Фан-ремастер Cardfight!! Vanguard базовой эры (2011), формат Classic. MVP: бустер BT01 + триал-деки TD01/TD02 — 94 карты, 124 принта.

Стек:

  • сервер — .NET 10, планируются Orleans, PostgreSQL, Redis;
  • клиент — Unity 6 (ещё не начат);
  • лаунчер — WPF (ещё не начат);
  • контент — YAML + C#-скрипты карт;
  • транспорт — WebSocket/TCP + MessagePack (решение зафиксировано, кода ещё нет).

Репозиторий:

  • ветка master;
  • remote ssh://git@192.168.31.158:2222/mpakobecex/VanguardReMaster.git;
  • последний коммит на момент передачи — 99532b6;
  • рабочее дерево чистое.

Главные принципы:

  • сервер авторитарный, клиент тонкий;
  • скрытая информация (колода, рука противника, закрытый урон, seed) не уходит клиенту;
  • Vanguard.Rules — чистая библиотека без сети, БД и Orleans: команда на вход, события на выход;
  • каждый блок работы коммитится и пушится в master.

2. Текущее состояние

Проверка Результат
Тесты 164/164 (131 правила/контракты + 33 сервер)
validate 0 ошибок, 0 предупреждений
translations check 94/94
stress 1000 1000/1000 партий, 0 ошибок
Карты 94/94 (66 с текстом + 28 ванильных)
Протокол и контракты docs/protocol.md, Vanguard.Shared готовы
Сервер сеть, матчмейкинг, реконнект, PostgreSQL + Redis, Docker
Клиент / лаунчер не начаты

Блок «Игровое ядро» из IMPLEMENTATION_CHECKLIST.md закрыт по его критериям: боты доигрывают 1000 партий, инварианты держатся, реплей воспроизводится бит-в-бит по хэшу событий.


3. Как проверить

dotnet установлен в %LOCALAPPDATA%\dotnet, поэтому в PowerShell:

$env:PATH = "$env:LOCALAPPDATA\dotnet;$env:PATH"

dotnet test server\Vanguard.slnx
dotnet run --project server\src\Vanguard.Cli -- validate
dotnet run --project server\src\Vanguard.Cli -- translations check
dotnet run --project server\src\Vanguard.Cli -- stress 1000

Ожидаемо:

Пройден! : не пройдено 0, пройдено 131     (Vanguard.Rules.Tests)
Пройден! : не пройдено 0, пройдено 33      (Vanguard.Server.Tests)
OK: ошибок 0, предупреждений 0
Переведено: 94/94
Партий: 1000, завершено: 1000, ошибок: 0

Сервер локально:

dotnet run --project server\src\Vanguard.Server --urls http://localhost:5080
# GET http://localhost:5080/health

Тестовый стенд на ssh home (Linux, Docker):

ssh home
cd ~/VanguardReMaster
# .env: VANGUARD_PORT=18080, VANGUARD_WS_URL=ws://192.168.31.158:18080/game
docker compose up -d --build
curl http://127.0.0.1:18080/health
# остановить: docker compose down

Storage:Provider=None по умолчанию: локальный запуск без базы. Стенд использует Postgres, Redis и восстанавливает матчи после рестарта.

Solution — server/Vanguard.slnx, не .sln.


4. Что сделано

4.1. Контент

  • 94 карты, 124 принта в content/sets/BT01.yaml, TD01.yaml, TD02.yaml.
  • Импортёр tools/import_cards.py.
  • Валидатор контента: щиты, Sentinel, параллельные арты, лимиты колоды.
  • Токенизация текстов, content/keywords.yaml.
  • Русские переводы content/translations/ru.yaml — 94/94.
  • Канон перевода docs/translation-rules.md v0.8.

4.2. Игровое ядро

server/src/Vanguard.Rules/Engine/GameEngine.cs.

Реализовано:

  • фазы Mulligan → Stand → Draw → Ride → Main → Battle → End;
  • стартовый Авангард, маллиган, добор;
  • райд, призыв, перемещение в пределах колонки (занятый круг меняется местами);
  • атака по Авангарду и переднему ряду Арьергарда;
  • поддержка, перехват, идеальная защита (Sentinel);
  • drive check и damage check с выбором целей;
  • триггеры: Critical, Stand, Draw, Heal;
  • победа/поражение, детерминированный RNG;
  • стоимости: Counter Blast, Soul Blast, сброс, поворот себя, уничтожение себя;
  • поиск в колоде без просмотра, поиск с призывом и с райдом;
  • просмотр верхней карты;
  • способности: Triggered, Continuous, Activated;
  • модификаторы силы/крита с источником и сроком жизни (StatModifier);
  • выдача и снятие способностей (GrantedAbility);
  • запреты (CardRestriction): не вставать, не атаковать, не поддерживать, не перехватывать, не быть выбранным;
  • снятие Сдерживания эффектом;
  • возврат карты с поля в колоду после боя и в конце хода;
  • способности из руки, сброса и круга защиты;
  • AskUnitMultiChoice для «до N» / «ровно N»;
  • LegalCommands() — все легальные команды текущего момента;
  • GameInvariants.Check() — проверка 50 карт, уникальных instanceId, урона ≤ 6.

Триггеры (AbilityTrigger):

OnRideOver, OnPlace, OnBoost, OnBoosted, OnBoostedHit, OnDriveCheck, OnGuard, OnRetireFromRC, OnOpponentRetire, OnLeaveGuardian, OnPhaseStart, OnAttackDeclared, OnAttackHit, OnDamageTaken (объявлен, но пока не вызывается — карт с ним нет).

4.3. Карты

  • 59 скриптов в CardScripts.cs, CardScriptsBT01.cs, CardScriptsTD.cs.
  • 6 карт Sentinel/Heal обрабатываются данными и движком без скрипта.
  • Tejas реализован флагом snipeBackRow в YAML.
  • 28 ванильных карт скрипта не требуют.
  • Итого 66/66 карт с текстом закрыты.

4.4. Реплей

server/src/Vanguard.Rules/Replay/:

  • CommandCodec — команда в стабильную строку и обратно;
  • MatchRecord — seed, колоды, команды, хэш событий;
  • MatchRecorder — записывает только принятые команды;
  • ReplayRunner — воспроизводит и проверяет хэш;
  • EventHasher — FNV-1a по тексту событий.

Запись переживает JSON и воспроизводится бит-в-бит.

4.5. Диагностика

  • server/src/Vanguard.Rules/Diagnostics/GameInvariants.cs;
  • CLI-команда stress [партий] — случайный бот, лимит 5000 шагов на партию.

4.6. Тесты

server/tests/Vanguard.Rules.Tests/GameEngineTests.cs — 117 тестов:

  • механики фаз, боя, защиты, перехвата, триггеров;
  • все ключевые карты и стоимости;
  • 50 детерминированных партий случайного бота с инвариантами;
  • JSON-реплей полной партии с проверкой хэша.

server/tests/Vanguard.Rules.Tests/ProtocolTests.cs — 12 тестов:

  • round-trip всех кадров клиента и сервера через MessagePack, включая вложенные union'ы MatchView и EventView;
  • отказ на пустом и слишком большом кадре;
  • соответствие интентов и GameCommand в обе стороны;
  • скрытие руки противника, колоды и закрытого урона;
  • отсутствие seed/RNG в DTO;
  • LegalIntents только у решающего игрока;
  • скрытие поиска в колоде от противника и сохранение глобальных номеров событий при фильтрации.

4.7. Сеть: протокол и контракты

  • docs/protocol.md — версия 1: WebSocket + MessagePack, handshake по одноразовому тикету, intentId и идемпотентность, eventSeq и resync, heartbeat, реконнект 0/2/10/30 с, таймер 90 с + банк 5 мин, авто-поражение через 5 минут отсутствия, таблица видимости зон.
  • Vanguard.Shared — мультитаргет netstandard2.1;net10.0:
    • netstandard2.1 — DTO и MessagePack для Unity, без ссылки на правила;
    • net10.0 — дополнительно проекция состояния поверх Vanguard.Rules.
  • DTO: кадры клиента и сервера, 18 интентов, 30 событий, MatchView, PlayerView, CardView, ClockView.
  • ProtocolResolver — StandardResolver + интернирование строк; MessagePackSecurity.UntrustedData; лимит кадра 256 КиБ.
  • MatchProjection — отдельный MatchView на каждого игрока; EventProjection — фильтрация скрытых событий и диапазон по eventSeq; IntentMapping — единственное место связи DTO с командами движка.
  • Проекция вызывается для наблюдателя без побочных эффектов и не трогает LegalCommands() чужого игрока (иначе утекала бы рука решающего).

4.8. Сервер

server/src/Vanguard.Server — ASP.NET Core 10, один процесс. Матчи, очередь и тикеты сохраняются в PostgreSQL/Redis, если они настроены; по умолчанию (Storage:Provider=None) сервер работает без внешних сервисов.

  • HTTP: POST /api/game/queue, GET /api/game/queue/{id}, DELETE /api/game/queue/{id}, POST /api/game/tickets, GET /health. Контракт — docs/server-api.md.
  • Матчмейкинг: две очереди (Classic Ranked / Casual), скрытый MMR, окно ±100 с расширением на 50 каждые 5 с до 2000, выбор ближайшего MMR. Ранкед меняет MMR по Эло (K = 32), casual — нет.
  • Тикеты: одноразовые, TTL 60 с, привязаны к (playerId, matchId, seat); при настроенном Redis изымаются через GETDEL (одноразовость между инстансами), иначе — из памяти.
  • IMatchGrain: 1 матч = 1 экземпляр, все входы (интенты, подключения, тики) проходят через один семафор; внутри нет ни ASP.NET Core, ни WebSocket — только IMatchConnection. Orleans-хостинг подключается поверх контракта.
  • WebSocket /game: handshake, MessagePack, intentId, idempotency, eventSeq, resync, heartbeat (20 с простоя → ping, ещё 20 с без ответа → 4408), реконнект, захват сессии (4409), rate limit 16 интентов/с.
  • Таймеры: 90 с на решение + банк 300 с, отсутствие 300 с → поражение; ServerClock каждые 5 с.
  • Результат: сдача, таймер и отсутствие завершают партию через GameEngine.Finish(winner); событие GameFinished уходит обоим, результат пишется в профили.
  • Проекция: своя рука, свой выбор, свои легальные интенты; у противника только количества и открытые зоны. seed и RNG не покидают сервер.
  • PostgreSQL: таблицы matches и match_intents. В заголовке — seed, колоды, игроки, часы, банки, статус; в журнале — intentId, команда, результат. При старте незавершённые матчи восстанавливаются повтором принятых команд на детерминированном движке, включая ожидания выбора: делегаты движка не сериализуются. Журнал реплея сверяется по eventSeq. Банк и дедлайн пишутся каждые ~5 с; потеря при аварии — не больше тика.
  • Redis: тикеты и записи очереди (JSON + sorted set по режимам, TTL); очередь поднимается при старте. Ключи с префиксом vanguard:.
  • Конфигурация: Storage:Provider = None / Memory / Postgres, Storage:ConnectionString; Redis:Configuration. Memory — EF InMemory для тестов, Postgres — постоянное хранилище.
  • Docker: Dockerfile + docker-compose.yml поднимают postgres, redis (AOF) и сервер; /health на http://localhost:8080 (порт меняется через VANGUARD_PORT, адрес WebSocket — VANGUARD_WS_URL).
  • Тесты (Vanguard.Server.Tests, 33):
    • тикеты: одноразовость и TTL;
    • heartbeat: ping после 20 с простоя, закрытие через 20 с, сброс любым кадром;
    • grain: attach, чужое место, duplicate intent, чужой ход, сдача, команды после финала, банк времени, отсутствие, старт часов, reconnect с eventSeq, захват сессии;
    • матчмейкинг: подбор, разные очереди, окно, плохая колода, отсутствие стартового Авангарда, отмена, замена очереди, истечение, MMR, перезагрузка очереди из хранилища;
    • персистентность: EF-хранилище, восстановление матча повтором команд, MatchRecoveryService после «рестарта»;
    • внешние хранилища (ExternalStorageTests, запускаются при VANGUARD_TEST_POSTGRES / VANGUARD_TEST_REDIS): реальные PostgreSQL и Redis, изъятие тикета, очередь;
    • E2E: два клиента играют полный матч по WebSocket и сходятся в победителе; обрыв и возврат по новому тикету с догоном состояния.

Проверено на ssh home: 164/164 тестов, включая реальные PostgreSQL и Redis; docker compose up поднимает стенд, матч создаётся через HTTP-очередь, а после docker compose restart server восстанавливается (Recovery: 1 of 1).


5. Структура и ключевые файлы

/server/src/Vanguard.Rules
  /Engine/GameEngine.cs              основной движок (~2500 строк)
  /Engine/GameState.cs               состояние, зоны, InTransit
  /Engine/GameModels.cs              CardInstance, Step, Phase, PendingChoice
  /Engine/GameCommands.cs            команды
  /Engine/GameEvents.cs              события
  /Engine/Modifiers.cs               StatModifier, GrantedAbility, CardRestriction
  /Engine/Abilities/Abilities.cs     AbilityDefinition, ActivatedAbility, триггеры
  /Engine/Abilities/CardScripts.cs   реестр Default + базовые карты
  /Engine/Abilities/CardScriptsBT01.cs
  /Engine/Abilities/CardScriptsTD.cs
  /Cards/CardDefinition.cs           статы, Sentinel, Restraint, snipeBackRow
  /Decks/DeckValidator.cs            правила колоды
  /Replay/                           запись и воспроизведение
  /Diagnostics/GameInvariants.cs
/server/src/Vanguard.Content          загрузка YAML и валидация
/server/src/Vanguard.Cli              validate, stats, translations, stress, deck
/server/src/Vanguard.Shared
  /Protocol/*.cs                      кадры, интенты, события, view, кодек
  /Projection/*.cs                    проекция состояния, фильтр событий, маппинг
/server/src/Vanguard.Server
  /Program.cs                         ASP.NET Core, DI, WebSocket endpoint
  /Api/                               очередь, тикеты, /health
  /Matchmaking/                       очереди, MMR, Redis-хранилище очереди
  /Matches/                           IMatchGrain, таймеры, тикер, регистр
  /Realtime/                          WebSocket-обвязка, чтение кадров
  /Tickets/                            одноразовые тикеты: память или Redis
  /Storage/                            EF-хранилище, восстановление матчей
  /Profiles/                           скрытый MMR и статистика
  /Dockerfile
/server/tests/Vanguard.Rules.Tests   131 тест
/server/tests/Vanguard.Server.Tests  33 теста, включая E2E и внешние хранилища
/content/sets                         карты
/content/translations/ru.yaml         переводы
/docs/translation-rules.md            канон перевода
/docs/protocol.md                     протокол матча, версия 1
/docs/server-api.md                   HTTP-очередь, тикеты, /health
/docs/HANDOFF.md                      этот файл
/docker-compose.yml                   postgres, redis, server
/.env.example                         VANGUARD_PORT и VANGUARD_WS_URL

6. Принятые решения

Их не нужно пересматривать без причины.

  • Формат называется Classic, не Standard.
  • Имена карт английские; nameRu зарезервировано.
  • Термины: Авангард, Арьергард, ранг, смена, поддержка, проверка, фазы Подъёма/Добора/Смены/Основная/Боевая/Завершающая, завершающий шаг, Сдерживание, «вы», буква ё.
  • Способности ищутся по nameId, а не по принту.
  • Лимит 4 копий — по nameId, только правило декбилдера.
  • Дубль в коллекции — по printingId; порог 4, замена внутри редкости.
  • Бустер: 5 карт = 4 C + 1 R+; веса R 70 / RR 22 / RRR 7 / SP 1.
  • Gold за матч: 30/15/20 (победа/поражение/ничья); Gold = XP / 5.
  • Таймер хода 90 с + банк 5 мин.
  • Транспорт матча — WebSocket + MessagePack; TCP с префиксом длины — резерв для Unity, если WebSocket не подойдёт.
  • Тикет на подключение — одноразовый, TTL 60 с, передаётся в ClientHello, не в URL; reconnect получает новый тикет.
  • playerId в HTTP-API — dev-заглушка до JWT. Выставлять сервер в интернет в таком виде нельзя.
  • Состояние сервера: PostgreSQL — снапшот/журнал матчей, Redis — тикеты и очередь; без конфигурации работает in-memory. IMatchGrain отделён от транспорта, Orleans подключается поверх.
  • Rate limit — 16 интентов в секунду на соединение.
  • Скрытый MMR: старт 1000, Эло K = 32, casual рейтинг не меняет.
  • Порядок триггеров выбирает игрок; опциональные способности спрашиваются.
  • Непрерывные эффекты пересчитываются при каждом запросе.
  • Поиск в колоде: 0 — уведомление и перемешивание, 1 — сразу, 2+ — выбор.
  • Эффекты не вкладываются: цепочка строго друг за другом.
  • Каждый блок — коммит и push в master.

7. Что осталось

7.1. Ближайший блок — сервер

Сделано в этом блоке:

  1. docs/protocol.md — версия 1: кадры, handshake по тикету, intent с intentId и eventSeq, resync, реконнект 0 / 2 / 10 / 30 с, отсутствие 5 минут → авто-поражение, таймер 90 с + банк 5 мин, heartbeat, коды ошибок, таблица видимости.
  2. Vanguard.Shared: DTO интентов и событий, MessagePack-резолвер, проекция состояния на каждого игрока, фильтрация скрытых событий, маппинг интентов.
  3. Vanguard.Server — сетевой матч работает: ASP.NET Core + WebSocket, тикеты, IMatchGrain (in-process), матчмейкинг, MMR, идемпотентность, таймеры, реконнект, /health, Dockerfile и compose. Контракт HTTP — docs/server-api.md.
  4. Проекция состояния подключена к грану и проверена E2E-тестом: своя рука и свой выбор полностью, у противника только количества и открытые зоны, seed и RNG не уходят.
  5. Docker Compose — postgres, redis (AOF) и сервер; /health, порт через VANGUARD_PORT.
  6. Персистентность:
    • PostgreSQL — заголовок матча + журнал интентов, восстановление повтором команд после рестарта, MatchRecoveryService;
    • Redis — одноразовые тикеты (GETDEL) и очередь с перезагрузкой.

Осталось в серверном направлении:

  1. Orleans — заменить in-process регистр на настоящий silo и граны. IMatchGrain уже отделён от транспорта и ASP.NET Core; персистентность и проекция от хостинга не зависят.
  2. JWT и мета-API — playerId перестанет быть dev-заглушкой.

Критерий готовности серверного блока выполнен: два клиента играют полный матч по сети; kill клиента на любом шаге восстанавливается через новый тикет и resumeFromSeq. Рестарт сервера пока теряет матчи: это закрывается пунктом 7.

7.2. Мета-API

  • User, BCrypt, JWT (access 15 мин + refresh 30 дней, ротация).
  • register, login, refresh, logout, me.
  • Коллекция по принтам, без верхнего предела.
  • Колоды: CRUD, validate, лимит 30, шаринг по коду.
  • Магазин, бустеры, триал-деки, токены.
  • Пыль: разбор C 5 / R 10 / RR 25 / RRR 100, крафт ×4, SP не крафтится.
  • Онбординг: триал-дека + 5 токенов.
  • Матчмейкинг, результат матча, Gold/XP, история.
  • Rate limiting.

7.3. Клиент

  • Unity 6 LTS, URP, Addressables, Input System, Localization, DOTween.
  • Сцены Boot, Meta, Battle.
  • VContainer, ApiClient, GameConnection.
  • Экраны: логин, меню, магазин, коллекция, декбилдер, очередь, настройки.
  • Боевая сцена: поле, рука, зоны, анимации, таймер.
  • Локализация RU/EN на лету.

7.4. Лаунчер

  • WPF: обновления, запуск клиента, выбор сервера.

7.5. Мелкие технические долги

  • OnDamageTaken не вызывается — добавить, когда появится карта с таким триггером.
  • Глобальный лимит шагов внутри движка не enforced — только в стресс-тесте.
  • GameEngine.cs большой; при росте разнести на partial-файлы.
  • MatchRecord сейчас JSON; для сети позже MessagePack.
  • В LegalCommands() не входят активируемые способности со стоимостью «сброс карты» — для UI их нужно будет перечислить с вариантами сброса.

8. Порядок действий в новой сессии

  1. Прочитать docs/HANDOFF.md, IMPLEMENTATION_CHECKLIST.md, README.md.
  2. Прогнать проверки из раздела 3.
  3. Следующий блок — Orleans и мета-API: заменить in-process регистр на silo, затем JWT, аккаунты, коллекция, колоды и магазин. Сетевой матч, PostgreSQL/Redis и Docker уже готовы: docs/protocol.md, Vanguard.Shared, Vanguard.Server, docs/server-api.md.
  4. После каждого блока: тесты, validate, translations check, коммит, push.
  5. Не ломать Vanguard.Rules: сервер должен зависеть от него, а не наоборот.

9. Подводные камни

  • Кодировка. Не использовать PowerShell Get-Content/Set-Content для UTF-8 файлов с русским текстом: однажды это уже испортило комментарии. Правки — только через edit/write, либо Python с encoding="utf-8".
  • PATH. dotnet лежит в %LOCALAPPDATA%\dotnet; без добавления в PATH команды не находятся.
  • Solution. server/Vanguard.slnx, не .sln.
  • Тесты. StartedEngine() принудительно ставит Conroe стартовым Авангардом, чтобы Barcgal не срабатывал.
  • Shared. Мультитаргет: файлы Projection/ компилируются только для net10.0; в netstandard2.1 (Unity) ссылки на Vanguard.Rules нет.
  • Видимость. LegalCommands() описывает решающего игрока. Вызывать его для наблюдателя нельзя — в списке есть printingId из чужой руки.
  • Сервер: rate limit. 16 интентов в секунду на соединение. Быстрый бот в E2E специально притормаживается; живой UI в лимит не упирается.
  • Сервер: хранилище. По умолчанию Storage:Provider=None — матчи живут в памяти. Для рестарта нужен Postgres + Storage:ConnectionString; схема создаётся EnsureCreated, миграций пока нет. Memory — EF InMemory.
  • Сервер: снапшот матча. Сериализуется не состояние движка, а seed, колоды, игроки, часы и журнал интентов. При восстановлении команды проигрываются заново; EventSeq обязан совпасть, иначе матч пропускается с ошибкой. Делегаты PendingChoice/Continuation восстанавливаются самим реплеем.
  • Сервер: восстановление. MatchRecoveryService поднимает только незавершённые матчи за последние 7 дней. Завершённые остаются историей в PostgreSQL и не восстанавливаются в память. Банк/дедлайн пишутся раз в ~5 с, поэтому авария теряет не больше тика.
  • Сервер: Redis. Тикеты изымаются через GETDEL (Redis 6.2+; в compose 7). Очередь — JSON + sorted set по режимам, TTL в два раза больше очереди. Префикс ключей — Redis:Prefix (vanguard:).
  • Сервер: Docker. На домашнем сервере порт 8080 занят, поэтому стенд поднимается с VANGUARD_PORT=18080 и VANGUARD_WS_URL в .env. docker compose restart server проверяет восстановление матчей.
  • Сервер: внешние тесты. VANGUARD_TEST_POSTGRES и VANGUARD_TEST_REDIS включают ExternalStorageTests; без них тесты проходят как no-op. Postgres-тест делает EnsureDeleted, поэтому БД должна быть тестовой.
  • Сервер: тесты. E2E поднимает WebApplicationFactory<Program>, играет полный матч по WebSocket и проверяет, что победитель совпал. Случайные партии завершаются за ~350 действий; таймаут теста — 120 с.
  • Сервер: таймеры. Тик раз в секунду. После 90 с банк списывается по фактическому превышению; TurnDeadlineMs в этот момент сдвигается, поэтому UI должен показывать ещё и BankMs.
  • Сервер: часы. Отсчёт стартует только когда подключились оба места. До этого ClockView.IsRunning = false, но таймер отсутствия уже идёт.
  • Сервер: проекция. ServerSnapshot и ServerEvents собираются отдельно на каждое место; не переиспользовать один MatchView на двоих.
  • Порядок фаз. Вход в Основную — EnterMainPhase() с OnPhaseStart.
  • Отложенные продолжения. State.Continuation — что выполнить после паузы; State.PendingQueue + _resolvingDepth запрещают вложенность эффектов.
  • OfferAbility. Возвращает Skipped / Paused / Resolved; вызывающий код продолжает очередь только при Skipped. Ошибка здесь уже приводила к потере шага защиты.
  • InTransit. Найденная карта, ждущая выбора круга, лежит в State.InTransit, иначе инвариант «50 карт» падает.
  • Задний ряд. Обычные карты атакуют только передний ряд; Tejas — задний в своей колонке через snipeBackRow.
  • Реестр карт. Новая карта без записи в CardScripts.Default молча останется без способностей; проверять покрытие скриптом.
  • Тестовые колоды. BuildDeck() в тестах не содержит все карты; для конкретной карты использовать MakeCard() и класть вручную.