447 lines
28 KiB
Markdown
447 lines
28 KiB
Markdown
# Передача сессии
|
||
|
||
Состояние проекта на **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()` и класть вручную.
|