VanguardReMaster/docs/HANDOFF.md

725 lines
50 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**. Документ нужен, чтобы новая сессия
продолжила работу без повторного разбора истории. Краткая оперативная
сводка — `docs/SESSION-STATE.md`; этот файл хранит подробную историю.
---
## 1. Проект
Фан-ремастер **Cardfight!! Vanguard** базовой эры (2011), формат **Classic**.
MVP: бустер **BT01** + триал-деки **TD01/TD02** — 94 карты, 124 принта.
Стек:
- сервер — **.NET 10**, **Orleans** (опционально), PostgreSQL, Redis;
- клиент — **Unity 6000.6.6f1**, URP, 3D-сцены (каркас готов);
- лаунчер — **WPF** (ещё не начат);
- контент — YAML + C#-скрипты карт;
- транспорт — **WebSocket + MessagePack** (реализован в сервере и клиенте).
Репозиторий:
- ветка `master`;
- remote `ssh://git@192.168.31.158:2222/mpakobecex/VanguardReMaster.git`;
- актуальный HEAD смотреть через `git log -1` (на момент записи — `6ed0998`);
- рабочее дерево чистое.
Главные принципы:
- сервер авторитарный, клиент тонкий;
- скрытая информация (колода, рука противника, закрытый урон, 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, Orleans, Docker |
| Мета-API | JWT, коллекция, колоды, магазин, экономика, история, реплеи |
| Клиент | Unity 6.6, 3D-сцены Login/Lobby/Collection/DeckBuilder/Battle, логин/коллекция/колоды/очередь/gray-box бой |
| Лаунчер | не начат |
Блок «Игровое ядро» из `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, пройдено 44 (Vanguard.Server.Tests)
OK: ошибок 0, предупреждений 0
Переведено: 94/94
Партий: 1000, завершено: 1000, ошибок: 0
```
Клиент (Unity):
```powershell
& 'C:\Program Files\Unity\Hub\Editor\6000.6.6f1\Editor\Unity.exe' `
-batchmode -nographics -projectPath client `
-runTests -testPlatform EditMode -testResults client\TestResults.xml
& 'C:\Program Files\Unity\Hub\Editor\6000.6.6f1\Editor\Unity.exe' `
-batchmode -nographics -projectPath client `
-executeMethod Vanguard.Editor.SceneSmoke.Run
```
Сервер локально:
```powershell
dotnet run --project server\src\Vanguard.Server --urls http://localhost:5080
# GET http://localhost:5080/health
```
Тестовый стенд на `ssh home` (Linux, Docker):
```bash
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. Принятые решения
Их не нужно пересматривать без причины.
- **Общение:** ответы короткие, без повторов и длинных вступлений.
- **Сомнения:** любое сомнение/вопрос — диалог с 2–4 вариантами; выбор
делает пользователь. Не угадывать.
- Формат называется **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. **[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`** — postgres, redis (AOF) и сервер; `/health`,
порт через `VANGUARD_PORT`. Сервер запускается с `Orleans:Enabled=true`.
6. **[x] Персистентность**:
- PostgreSQL — заголовок матча + журнал интентов, восстановление повтором
команд после рестарта, `MatchRecoveryService`;
- Redis — одноразовые тикеты (`GETDEL`) и очередь с перезагрузкой.
Осталось в серверном направлении:
7. **Мета-API** — сделано: JWT, аккаунты, коллекция, колоды, магазин,
экономика, онбординг, матчмейкинг по `deckId`, история и реплеи.
`playerId` остался только как dev-режим при `Game:AllowDevQueue=true`.
8. **[x] 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`);
сцены и `SceneFlow` уже созданы, UI пока code-generated gray-box;
- сцена `Boot` + runtime-инициализатор, поэтому клиент стартует и в пустой
сцене;
- composition root на VContainer: `AppConfig`, `TokenStore`, `ApiClient`,
`AuthService`, `MetaService`, `PlayerState`, `SceneFlow`;
- 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 карт);
- магазин в Lobby: `/api/store`, бустер за Gold/токен с показом 5 карт,
покупка второй триал-деки;
- история матчей и реплей: `/api/matches`, `/api/replays/{id}`, просмотр
событий и кадров состояния;
- очередь classic-ranked, опрос статуса, отмена, тикет и `wsUrl`;
- `GameConnection` (WebSocket + MessagePack), handshake `ClientHello`,
snapshot/events, интенты с ответом сервера, ping/pong, закрытие;
- серый боевой экран: доска, рука, часы, все легальные интенты кнопками,
сдача с подтверждением, результат;
- `Vanguard.Shared.dll` (netstandard2.1) и MessagePack 3.1.12 подключены
как плагины; `client/README.md` описывает запуск и ограничения;
- сцены `Login`, `Lobby`, `Collection`, `DeckBuilder`, `Battle` и
`SceneFlow`; `Boot` — стартовая служебная сцена, `VanguardApp` и сервисы
живут между сценами;
- 3D-фон на каждой сцене: камера, свет, пол и тематические примитивы;
UI пока code-generated uGUI;
- EditMode smoke `Vanguard.Tests.Editor` и `Vanguard.Editor.SceneSmoke`
проверяют запуск Boot → Login и создание экранов/3D-окружения;
live-probe (не входит в набор) проверил онбординг, коллекцию,
создание/удаление колоды, а также два WebSocket-подключения, маллиган,
сдачу и `GameFinished` против стенда.
Дальше по клиенту:
1. Боевой UI: арты, зоны, анимации, скип/ускорение; реконнект и таймауты.
2. Полный декбилдер: фильтры, счётчики, валидация.
3. 3D-наполнение сцен: модели/арт, prefab-экраны, камера.
4. Локализация RU/EN, настройки, DOTween.
**Зафиксированное визуальное направление** — `docs/client-visual-direction.md`:
3D-клиент на URP, отдельные сцены-экраны Login / Lobby / Collection /
DeckBuilder / Battle; Boot и общие сервисы остаются персистентными.
Сцены и `SceneFlow` уже созданы; UI пока code-generated 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/SESSION-STATE.md`, `docs/HANDOFF.md`,
`IMPLEMENTATION_CHECKLIST.md`, `README.md`.
2. Прогнать проверки из раздела 3.
3. Следующий блок — клиент: магазин/бустеры в Lobby и история/реплей,
затем полный декбилдер и 3D-наполнение сцен. Сервер и мета-API готовы:
`docs/protocol.md`, `docs/server-api.md`, `Vanguard.Shared`.
4. Перед началом сверить `docs/client-visual-direction.md`: 3D-сцены,
`Boot` и общие сервисы персистентны.
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()` и класть вручную.
## 2026-10-21 — декбилдер и каталог карт
- Добавлен `GET /api/cards` (Bearer): все принты с `grade`, `trigger`,
`clan`, `rarity`, `isSentinel`, `isParallel`, `basePrintingId`.
Клиент объединяет каталог с коллекцией по `printingId`; правила для
фильтров/статистики не дублируются в клиенте.
- Новый `DeckBuilderView`: список колод → создание/редактирование.
В редакторе: фильтры в оверлее (grade/trigger/clan) и отдельный сброс,
статистика по триггерам и грейдам, коллекция сеткой, справа
горизонтальная зона текущей колоды, сохранение через POST/PUT.
- Ограничения в UI: 50 карт, ≤4 по `nameId`, не больше owned-копий;
серверная валидация остаётся источником истины.
- `DeckBuilderSceneController` и `CollectionSceneController` теперь
вызывают `Show()`; раньше сцены создавали скрытый корень.
- `MetaService.GetCatalogAsync`, `UpdateDeckAsync`; `ApiClient.PutAsync`.
- Проверки: серверные 131 + 44 на `ssh home`; Unity EditMode 2/2,
`SCENE_SMOKE success=True active=Login`; live `GET /api/cards` — 124
принта.