VanguardReMaster/docs/HANDOFF.md

447 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Передача сессии
Состояние проекта на **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:
```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
```
Сервер локально:
```powershell
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.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, один процесс, состояние в памяти.
- **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. Ближайший блок — сервер
Сделано в этом блоке:
1. **[x] `docs/protocol.md`** — версия 1:
кадры, handshake по тикету, `intent` с `intentId` и `eventSeq`,
resync, реконнект 0 / 2 / 10 / 30 с, отсутствие 5 минут → авто-поражение,
таймер 90 с + банк 5 мин, heartbeat, коды ошибок, таблица видимости.
2. **[x] `Vanguard.Shared`**:
DTO интентов и событий, MessagePack-резолвер, проекция состояния
на каждого игрока, фильтрация скрытых событий, маппинг интентов.
3. **[x] `Vanguard.Server`** — сетевой матч работает:
ASP.NET Core + WebSocket, тикеты, `IMatchGrain` (in-process),
матчмейкинг, MMR, идемпотентность, таймеры, реконнект, `/health`,
Dockerfile и compose. Контракт HTTP — `docs/server-api.md`.
4. **[x] Проекция состояния** подключена к грану и проверена E2E-тестом:
своя рука и свой выбор полностью, у противника только количества
и открытые зоны, seed и RNG не уходят.
5. **[x] `Docker Compose`** — сервер с контентом на `http://localhost:8080`.
Осталось в серверном направлении:
6. **Orleans** — заменить in-process регистр на настоящий silo и граны.
`IMatchGrain` уже отделён от транспорта и ASP.NET Core.
7. **Персистентность**:
- PostgreSQL — снапшот матча, восстановление grain после рестарта;
- Redis — тикеты, очереди, кэш.
8. **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: PostgreSQL-снапшоты,
Redis для тикетов/очередей, замена in-process регистра на silo.
Сетевой матч уже работает: `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 в лимит не упирается.
- **Сервер: 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()` и класть вручную.