47 KiB
Передача сессии
Состояние проекта на 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.mdv0.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. Ближайший блок — сервер
Сделано в этом блоке:
docs/protocol.md— версия 1: кадры, handshake по тикету,intentсintentIdиeventSeq, resync, реконнект 0 / 2 / 10 / 30 с, отсутствие 5 минут → авто-поражение, таймер 90 с + банк 5 мин, heartbeat, коды ошибок, таблица видимости.Vanguard.Shared: DTO интентов и событий, MessagePack-резолвер, проекция состояния на каждого игрока, фильтрация скрытых событий, маппинг интентов.Vanguard.Server— сетевой матч работает: ASP.NET Core + WebSocket, тикеты,IMatchGrain(in-process), матчмейкинг, MMR, идемпотентность, таймеры, реконнект,/health, Dockerfile и compose. Контракт HTTP —docs/server-api.md.- Проекция состояния подключена к грану и проверена E2E-тестом: своя рука и свой выбор полностью, у противника только количества и открытые зоны, seed и RNG не уходят.
Docker Compose— postgres, redis (AOF) и сервер;/health, порт черезVANGUARD_PORT. Сервер запускается сOrleans:Enabled=true.- Персистентность:
- PostgreSQL — заголовок матча + журнал интентов, восстановление повтором
команд после рестарта,
MatchRecoveryService; - Redis — одноразовые тикеты (
GETDEL) и очередь с перезагрузкой.
- PostgreSQL — заголовок матча + журнал интентов, восстановление повтором
команд после рестарта,
Осталось в серверном направлении:
- Мета-API — сделано: JWT, аккаунты, коллекция, колоды, магазин,
экономика, онбординг, матчмейкинг по
deckId, история и реплеи.playerIdостался только как dev-режим приGame:AllowDevQueue=true. - 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);
- сцена
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), handshakeClientHello, 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против стенда.
Дальше по клиенту:
- Довести боевой UI: арты, зоны, анимации, скип/ускорение.
- Реконнект на всех шагах и аккуратная обработка таймаутов.
- Полный декбилдер с фильтрами и счётчиками.
- Магазин и открытие бустера.
- История матчей и реплей.
- Локализация RU/EN на лету, настройки, DOTween-анимации.
Старый план экранов остаётся в силе:
- Unity 6 LTS, URP, Addressables, Input System, Localization, DOTween.
- Сцены
Boot,Meta,Battle. - VContainer,
ApiClient,GameConnection. - Экраны: логин, меню, магазин, коллекция, декбилдер, очередь, настройки.
- История матчей и экран реплея:
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. Порядок действий в новой сессии
- Прочитать
docs/HANDOFF.md,IMPLEMENTATION_CHECKLIST.md,README.md. - Прогнать проверки из раздела 3.
- Следующий блок — клиент (Unity):
Boot/Meta/Battle,ApiClient,GameConnection, логин, коллекция, декбилдер, магазин, очередь, история и реплей. Сервер и мета-API готовы:docs/protocol.md,docs/server-api.md,Vanguard.Shared. - CI и Orleans закрыты; следующий блок — клиент. Перед началом: проверить
docs/protocol.md,docs/server-api.md,Vanguard.Shared. - После каждого блока: тесты,
validate,translations check, коммит, push. - Не ломать
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()и класть вручную.