28 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. Текущее состояние
| Проверка | Результат |
|---|---|
| Тесты | 158/158 (131 правила/контракты + 27 сервер) |
validate |
0 ошибок, 0 предупреждений |
translations check |
94/94 |
stress 1000 |
1000/1000 партий, 0 ошибок |
| Карты | 94/94 (66 с текстом + 28 ванильных) |
| Протокол и контракты | docs/protocol.md, Vanguard.Shared готовы |
| Сервер | полный матч по сети, матчмейкинг, реконнект (in-memory) |
| Клиент / лаунчер | не начаты |
Блок «Игровое ядро» из 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, пройдено 27 (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
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, один процесс, состояние в памяти.
- 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); для реконнекта выдаётся новый тикет. 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 не покидают сервер. - Docker:
Dockerfile+docker-compose.ymlподнимают сервер с контентом наhttp://localhost:8080. - Тесты (
Vanguard.Server.Tests, 27):- тикеты: одноразовость и TTL;
- heartbeat: ping после 20 с простоя, закрытие через 20 с, сброс любым кадром;
- grain: attach, чужое место, duplicate intent, чужой ход, сдача,
команды после финала, банк времени, отсутствие, старт часов,
reconnect с
eventSeq, захват сессии; - матчмейкинг: подбор, разные очереди, окно, плохая колода, отсутствие стартового Авангарда, отмена, замена очереди, истечение, MMR;
- E2E: два клиента играют полный матч по WebSocket и сходятся в победителе; обрыв и возврат по новому тикету с догоном состояния.
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
/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, расширяющееся окно
/Matches/ IMatchGrain, таймеры, тикер, регистр
/Realtime/ WebSocket-обвязка, чтение кадров
/Tickets/ одноразовые тикеты
/Profiles/ скрытый MMR и статистика
/Dockerfile
/server/tests/Vanguard.Rules.Tests 131 тест
/server/tests/Vanguard.Server.Tests 27 тестов, включая E2E по WebSocket
/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 сервер в контейнере
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. Выставлять сервер в интернет в таком виде нельзя.- Состояние сервера пока in-memory:
IMatchGrainотделён от транспорта, Orleans и PostgreSQL подключаются поверх. - Rate limit — 16 интентов в секунду на соединение.
- Скрытый MMR: старт 1000, Эло K = 32, casual рейтинг не меняет.
- Порядок триггеров выбирает игрок; опциональные способности спрашиваются.
- Непрерывные эффекты пересчитываются при каждом запросе.
- Поиск в колоде: 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— сервер с контентом наhttp://localhost:8080.
Осталось в серверном направлении:
- Orleans — заменить in-process регистр на настоящий silo и граны.
IMatchGrainуже отделён от транспорта и ASP.NET Core. - Персистентность:
- PostgreSQL — снапшот матча, восстановление grain после рестарта;
- Redis — тикеты, очереди, кэш.
- 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. Порядок действий в новой сессии
- Прочитать
docs/HANDOFF.md,IMPLEMENTATION_CHECKLIST.md,README.md. - Прогнать проверки из раздела 3.
- Следующий блок — персистентность и Orleans: PostgreSQL-снапшоты,
Redis для тикетов/очередей, замена in-process регистра на silo.
Сетевой матч уже работает:
docs/protocol.md,Vanguard.Shared,Vanguard.Server,docs/server-api.md. - После каждого блока: тесты,
validate,translations check, коммит, push. - Не ломать
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 в лимит не упирается.
- Сервер: in-memory. Очереди, тикеты, матчи и профили живут в памяти. Рестарт сервера теряет матчи; kill клиента и реконнект переживаются.
- Сервер: тесты. 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()и класть вручную.