VanguardReMaster/docs/HANDOFF.md

48 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. Текущее состояние

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

Блок «Игровое ядро» из 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, пройдено 44      (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-хост (MatchActorGrain) подключается поверх того же контракта; соединения живут в IMatchConnectionRegistry, чтобы состояние грана оставалось сериализуемым.
  • 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 с; потеря при аварии — не больше тика. Схема — EF-миграции (Migrations/InitialCreate), на старте Migrate.
  • Redis: тикеты и записи очереди (JSON + sorted set по режимам, TTL); очередь поднимается при старте. Ключи с префиксом vanguard:.
  • Конфигурация: Storage:Provider = None / Memory / Postgres, Storage:ConnectionString; Redis:Configuration. Memory — EF InMemory для тестов, Postgres — постоянное хранилище.
  • Orleans: Orleans:Enabled (false по умолчанию, true в compose). Включённый режим поднимает silo с localhost-clustering в том же процессе (Orleans:SiloPort / GatewayPort, по умолчанию 11111/30000); MatchActorGrain держит доменный MatchGrain, IMatchConnectionRegistry остаётся в процессе. Активный матч держится тикером, завершённые матчи выгружаются после CompletedMatchTtlSeconds; страховочный CollectionAge 12 часов. Один silo на инстанс; для нескольких реплик нужен настоящий кластер и внешний реестр соединений. Тест кластера — OrleansMatchTests.
  • Docker: Dockerfile + docker-compose.yml поднимают postgres, redis (AOF) и сервер; /health на http://localhost:8080 (порт меняется через VANGUARD_PORT, адрес WebSocket — VANGUARD_WS_URL).
  • CI: .gitea/workflows/ci.yml — restore, тесты, validate и translations check на push/PR. Actions включены для репозитория, home-runner выполняет job (проверено: все шаги зелёные). setup-dotnet указан явным URL с GitHub: в зеркале code.forgejo.org его нет.
  • Тесты серверного ядра (35 из 44 в Vanguard.Server.Tests):
    • тикеты: одноразовость и 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 и сходятся в победителе; обрыв и возврат по новому тикету с догоном состояния;
    • Orleans: грана инициализируется, принимает attach и интент, граница Orleans сериализуется (тестовый TestCluster); полный матч двух клиентов через HTTP/WebSocket при Orleans:Enabled=true.

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

4.9. Мета-API

  • Аккаунты: register, login, refresh, logout, me. BCrypt, JWT access 15 минут, refresh 30 дней с ротацией; в БД хранится хэш refresh-токена, старый после ротации отзывается.
  • Онбординг: одна из двух триал-дек, 50 карт в коллекцию и 5 токенов; повтор невозможен. Триал-деки — content/trial-decks.yaml.
  • Коллекция: по printingId, без верхнего предела; разбор C 5 / R 10 / RR 25 / RRR 100 пыли, крафт 20 / 40 / 100 / 400, SP нельзя.
  • Колоды: CRUD, валидация 50 карт / ≤4 по nameId / Sentinel / Heal / стартовый Авангард, проверка владения картами, лимит 30, шаринг по коду.
  • Магазин: бустер 100 Gold или 1 токен; вторая триал-дека 500 Gold; состав бустера 4 C + 1 R+ (R 70 / RR 22 / RRR 7 / SP 1) из content/pack_odds.yaml; duplicate protection по printingId с порогом 4.
  • Экономика: журнал транзакций, баланс вычисляется, idempotencyKey на всех покупках, разборе и крафте.
  • Очередь с JWT: { mode, deckId }, игрок и колода берутся из токена и коллекции. Dev-режим playerId + deck остался только при Game:AllowDevQueue=true.
  • Rate limiting: 60/мин на auth, 300/мин на прочие мутации, плюс глобальный лимит по IP.

4.10. История матчей и реплеи

  • MetaProfileService после подтверждённого завершения матча пишет историю обоим игрокам, меняет MMR в ранкеде и начисляет Gold/XP. Победа 30 Gold / 150 XP, поражение 15 / 75; сдача, явный выход, таймер и отсутствие дают 0, но остаются в истории.
  • GET /api/matches, GET /api/matches/{id} — история игрока.
  • GET /api/replays/{id} восстанавливает матч из seed, колод и журнала принятых команд и отдаёт:
    • events — события, видимые этому игроку;
    • frames — MatchView до и после каждой команды.
  • Реплей собирается под зрителя: рука противника и закрытый урон скрыты, legalIntents пуст, часы обнулены. Смотреть может только участник.
  • Проверено на ssh home: живой Postgres, регистрация → онбординг → бустер за токен → колода → очередь → матч; /api/replays/{id} отдаёт 11 стартовых событий и кадр, рука противника пуста. После рестарта сервера матч восстановлен (matches: 1).

Счётчик Vanguard.Server.Tests вырос до 44: добавлены auth, мета-профиль, мета-E2E (онбординг → бустер → колода → матч → история → реплей), проверки реплея, явного выхода и истории/реплея после выхода.


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
  /Auth/                              JWT, register/login/refresh, BCrypt
  /Meta/                              коллекция, колоды, магазин, экономика, история, реплеи
  /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  44 теста, включая E2E и внешние хранилища
/content/sets                         карты
/content/trial-decks.yaml             две триал-деки
/content/pack_odds.yaml               веса бустера
/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 рейтинг не меняет.
  • Аккаунты: BCrypt, access JWT 15 минут, refresh 30 дней с ротацией; в БД хранится только хэш refresh-токена.
  • Онбординг: одна триал-дека бесплатно + 5 токенов; вторая триал-дека за 500 Gold; бустер 100 Gold или 1 токен.
  • Коллекция по printingId без лимита; лимит 4 по nameId — только декбилдер.
  • Пыль: разбор C 5 / R 10 / RR 25 / RRR 100, крафт 20 / 40 / 100 / 400, SP не разбирается и не крафтится.
  • Матч: победа 30 Gold / 150 XP, поражение 15 / 75; Gold = XP / 5. Сдача, явный выход, таймер и отсутствие наград не дают.
  • История матча пишется обоим игрокам один раз; реплей доступен только участникам и собирается под зрителя, без руки противника.
  • Порядок триггеров выбирает игрок; опциональные способности спрашиваются.
  • Непрерывные эффекты пересчитываются при каждом запросе.
  • Поиск в колоде: 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. Сервер запускается с Orleans:Enabled=true.
  6. Персистентность:
    • PostgreSQL — заголовок матча + журнал интентов, восстановление повтором команд после рестарта, MatchRecoveryService;
    • Redis — одноразовые тикеты (GETDEL) и очередь с перезагрузкой.

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

  1. Мета-API — сделано: JWT, аккаунты, коллекция, колоды, магазин, экономика, онбординг, матчмейкинг по deckId, история и реплеи. playerId остался только как dev-режим при Game:AllowDevQueue=true.
  2. Orleans — MatchActorGrain + OrleansMatchAdapter; silo включается Orleans:Enabled=true, в compose включён. MatchGrain остался доменным ядром, соединения вынесены в IMatchConnectionRegistry, MatchActorGrain восстанавливается из снапшота на активации. Покрыто OrleansMatchTests (тестовый кластер) и OrleansServerE2ETests (полный матч через HTTP/WebSocket при включённом silo).

Критерий готовности серверного блока выполнен: два клиента играют полный матч по сети; kill клиента восстанавливается через новый тикет и resumeFromSeq; рестарт сервера восстанавливает матчи из PostgreSQL. CI подключён и проходит; серверный блок закрыт.

7.2. Мета-API

Сделано:

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

Осталось после MVP: косметика, квесты, баттлпасс, Draft, друзья/чат, история покупок в клиенте.

7.3. Клиент

Первый блок начат: Unity 6000.6.6f1, URP-проект в client/.

Сделано:

  • проект создан из URP-шаблона Unity 6.6; пакеты: URP, Input System, Addressables, Localization, Newtonsoft.Json, VContainer (OpenUPM);
  • визуальное направление зафиксировано: 3D, отдельные сцены-экраны Login/Lobby/Collection/DeckBuilder/Battle (docs/client-visual-direction.md); текущий единый Canvas — временный gray-box;
  • сцена Boot + runtime-инициализатор, поэтому клиент стартует и в пустой сцене;
  • composition root на VContainer: AppConfig, TokenStore, ApiClient, AuthService, PlayerState, AppRootView;
  • HTTP-клиент на UnityWebRequest + Newtonsoft.Json, разбор ApiError, таймаут 20 с;
  • login/register/refresh/me/logout, восстановление сессии по refresh, сохранение токенов в PlayerPrefs (dev);
  • экран входа с выбором адреса сервера (стенд по умолчанию http://192.168.31.158:18080) и главный экран с балансами, MMR и переходами в разделы;
  • онбординг: список триал-дек из /api/store, выбор одной, 50 карт + 5 токенов, обновление профиля;
  • коллекция: /api/collection, балансы, разбор в пыль с подтверждением;
  • колоды: /api/decks, удаление и автосборка стартовой колоды из выбранной триал-деки (50 карт);
  • очередь classic-ranked, опрос статуса, отмена, тикет и wsUrl;
  • GameConnection (WebSocket + MessagePack), handshake ClientHello, snapshot/events, интенты с ответом сервера, ping/pong, закрытие;
  • серый боевой экран: доска, рука, часы, все легальные интенты кнопками, сдача с подтверждением, результат;
  • Vanguard.Shared.dll (netstandard2.1) и MessagePack 3.1.12 подключены как плагины; client/README.md описывает запуск и ограничения;
  • EditMode и PlayMode smoke-тесты Vanguard.Tests.Editor / Vanguard.Tests.PlayMode собирают все экраны и проверяют запуск приложения; live-probe (не входит в набор) проверил онбординг, коллекцию, создание/удаление колоды, а также два WebSocket-подключения, маллиган, сдачу и GameFinished против стенда.

Дальше по клиенту:

  1. Перейти к 3D-визуалу: отдельные сцены Login, Lobby, Collection, DeckBuilder, Battle; Boot — служебная сцена.
  2. Довести боевой UI: арты, зоны, анимации, скип/ускорение.
  3. Реконнект на всех шагах и аккуратная обработка таймаутов.
  4. Полный декбилдер с фильтрами и счётчиками.
  5. Магазин и открытие бустера.
  6. История матчей и реплей.
  7. Локализация RU/EN на лету, настройки, DOTween-анимации.

Зафиксированное визуальное направление — docs/client-visual-direction.md: 3D-клиент на URP, отдельные сцены-экраны Login / Lobby / Collection / DeckBuilder / Battle; Boot и общие сервисы остаются персистентными. Текущий единый Canvas — временный gray-box, его разносим по сценам.

  • Unity 6 LTS, URP, Addressables, Input System, Localization, DOTween.
  • VContainer, ApiClient, GameConnection, SceneFlow.
  • Сцены: Boot (служебная), Login, Lobby, Collection, DeckBuilder, Battle; очередь/онбординг — оверлеи Lobby.
  • История матчей и экран реплея: GET /api/matches, GET /api/replays/{id}, проигрывание frames + events.
  • Локализация 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. Следующий блок — клиент (Unity): 3D-сцены Login, Lobby, Collection, DeckBuilder, Battle; Boot — служебная сцена. ApiClient, GameConnection, логин, коллекция, декбилдер, магазин, очередь, история и реплей. Визуальное решение — docs/client-visual-direction.md. Сервер и мета-API готовы: docs/protocol.md, docs/server-api.md, Vanguard.Shared.
  4. CI и Orleans закрыты; следующий блок — клиент. Перед началом: проверить docs/protocol.md, docs/server-api.md, Vanguard.Shared.
  5. После каждого блока: тесты, validate, translations check, коммит, push.
  6. Не ломать Vanguard.Rules: сервер должен зависеть от него, а не наоборот.

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

  • Клиент: HTTP. В Unity по умолчанию запрещены незащищённые соединения: для dev-стенда в client/ProjectSettings/ProjectSettings.asset стоит insecureHttpOption: 2 (Always). Для релиза — HTTPS и вернуть запрет.
  • Клиент: WebSocket. GameConnection использует System.Net.WebSockets.ClientWebSocket; это окно для Windows-десктопа. Для WebGL/мобильного IL2CPP нужен отдельный транспорт/плагин.
  • Кодировка. Не использовать 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: матчи живут в памяти (NullMatchStore), мета-API — в EF InMemory. Для рестарта нужен Postgres + Storage:ConnectionString; Memory — EF InMemory для всего. Схема — EF-миграции (Migrations/InitialCreate), на старте MigrateAsync; для InMemory — EnsureCreated. Миграции применяются автоматически.
  • Сервер: снапшот матча. Сериализуется не состояние движка, а 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 с.
  • Мета: JWT. Jwt:SigningKey — не короче 32 символов; дефолтный ключ только для разработки. Access 15 минут, refresh 30 дней с ротацией; в БД лежит SHA-256 от refresh, старый токен после ротации недействителен.
  • Мета: dev-очередь. Game:AllowDevQueue=true разрешает очередь по playerId без JWT. В compose выключено; не включать в production.
  • Мета: колода. Сохранение проверяет не только правила Classic, но и владение каждой картой. Триал-дека даёт карты в коллекцию, а не готовую колоду — её собирают декбилдером.
  • Мета: бустер. 4 C + 1 R+, веса в content/pack_odds.yaml. Duplicate protection считает printingId и порог 4; при насыщении редкости выбирается другая, иначе допускается дубль. SP не крафтится и не разбирается.
  • Мета: награды. Gold/XP только за normal; сдача, явный выход, таймер и отсутствие дают 0. История пишется один раз на (userId, matchId), повтор результата не дублирует валюту.
  • Реплей. Доступен только участнику; собирается из seed, колод и журнала принятых команд. frames — состояние после каждой команды, events — только видимые зрителю события; рука противника и закрытый урон скрыты. Сырые колоды и seed наружу не отдаются.
  • Сервер: таймеры. Тик раз в секунду. После 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() и класть вручную.