705 lines
49 KiB
Markdown
705 lines
49 KiB
Markdown
# Передача сессии
|
||
|
||
Состояние проекта на **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 карт,
|
||
покупка второй триал-деки;
|
||
- очередь 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. История матчей и реплей: `GET /api/matches`, `GET /api/replays/{id}`.
|
||
2. Полный декбилдер: фильтры, счётчики, валидация.
|
||
3. Боевой UI: арты, зоны, анимации, скип/ускорение; реконнект и таймауты.
|
||
4. 3D-наполнение сцен: модели/арт, prefab-экраны, камера.
|
||
5. Локализация 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()` и класть вручную.
|