Enforce front-row-only attacks and label support slots
CI / test (push) Successful in 2m34s
Details
CI / test (push) Successful in 2m34s
Details
This commit is contained in:
parent
cd2d40e4d4
commit
17c589ab1e
|
|
@ -640,7 +640,7 @@ namespace Vanguard.UI
|
||||||
AddCardBox(
|
AddCardBox(
|
||||||
grid,
|
grid,
|
||||||
card,
|
card,
|
||||||
slot == -1 ? "V" : "R" + (slot + 1),
|
slot == -1 ? "V" : slot <= 2 ? "SUP" + (slot + 1) : "R" + (slot + 1),
|
||||||
compact: false,
|
compact: false,
|
||||||
player.Index,
|
player.Index,
|
||||||
slot);
|
slot);
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ MVP: бустер **BT01** + триал-деки **TD01/TD02** — 94 карты
|
||||||
|
|
||||||
| Проверка | Результат |
|
| Проверка | Результат |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Тесты сервера | **176/176** (131 правила/контракты + 45 сервер) |
|
| Тесты сервера | **177/177** (132 правила/контракты + 45 сервер) |
|
||||||
| `validate` | 0 ошибок, 0 предупреждений |
|
| `validate` | 0 ошибок, 0 предупреждений |
|
||||||
| `translations check` | 94/94 |
|
| `translations check` | 94/94 |
|
||||||
| `stress 1000` | 1000/1000 партий, 0 ошибок |
|
| `stress 1000` | 1000/1000 партий, 0 ошибок |
|
||||||
|
|
@ -72,7 +72,7 @@ dotnet run --project server\src\Vanguard.Cli -- stress 1000
|
||||||
Ожидаемо:
|
Ожидаемо:
|
||||||
|
|
||||||
```
|
```
|
||||||
Пройден! : не пройдено 0, пройдено 131 (Vanguard.Rules.Tests)
|
Пройден! : не пройдено 0, пройдено 132 (Vanguard.Rules.Tests)
|
||||||
Пройден! : не пройдено 0, пройдено 45 (Vanguard.Server.Tests)
|
Пройден! : не пройдено 0, пройдено 45 (Vanguard.Server.Tests)
|
||||||
OK: ошибок 0, предупреждений 0
|
OK: ошибок 0, предупреждений 0
|
||||||
Переведено: 94/94
|
Переведено: 94/94
|
||||||
|
|
@ -305,7 +305,7 @@ Solution — **`server/Vanguard.slnx`**, не `.sln`.
|
||||||
Orleans сериализуется (тестовый `TestCluster`); полный матч двух
|
Orleans сериализуется (тестовый `TestCluster`); полный матч двух
|
||||||
клиентов через HTTP/WebSocket при `Orleans:Enabled=true`.
|
клиентов через HTTP/WebSocket при `Orleans:Enabled=true`.
|
||||||
|
|
||||||
Проверено на `ssh home`: 131 + 45 теста, включая реальные PostgreSQL и Redis;
|
Проверено на `ssh home`: 132 + 45 теста, включая реальные PostgreSQL и Redis;
|
||||||
`docker compose up` поднимает стенд, матч создаётся через HTTP-очередь,
|
`docker compose up` поднимает стенд, матч создаётся через HTTP-очередь,
|
||||||
а после `docker compose restart server` восстанавливается (`Recovery: 1 of 1`).
|
а после `docker compose restart server` восстанавливается (`Recovery: 1 of 1`).
|
||||||
|
|
||||||
|
|
@ -320,6 +320,10 @@ Solution — **`server/Vanguard.slnx`**, не `.sln`.
|
||||||
C 5 / R 10 / RR 25 / RRR 100 пыли, крафт 20 / 40 / 100 / 400, SP нельзя.
|
C 5 / R 10 / RR 25 / RRR 100 пыли, крафт 20 / 40 / 100 / 400, SP нельзя.
|
||||||
- **Колоды**: CRUD, валидация 50 карт / ≤4 по `nameId` / Sentinel / Heal /
|
- **Колоды**: CRUD, валидация 50 карт / ≤4 по `nameId` / Sentinel / Heal /
|
||||||
стартовый Авангард, проверка владения картами, лимит 30, шаринг по коду.
|
стартовый Авангард, проверка владения картами, лимит 30, шаринг по коду.
|
||||||
|
- **Правило атаки**: атакуют только Вэнгард и Арьергард переднего ряда
|
||||||
|
(слоты 3/4); задний ряд (0–2) — только поддержка/boost. Задний ряд
|
||||||
|
также нельзя атаковать без `snipeBackRow`. `LegalCommands` не предлагает
|
||||||
|
атаки из заднего ряда; добавлен регрессионный тест.
|
||||||
- **Магазин**: бустер 100 Gold или 1 токен; вторая триал-дека 500 Gold;
|
- **Магазин**: бустер 100 Gold или 1 токен; вторая триал-дека 500 Gold;
|
||||||
состав бустера 4 C + 1 R+ (R 70 / RR 22 / RRR 7 / SP 1) из
|
состав бустера 4 C + 1 R+ (R 70 / RR 22 / RRR 7 / SP 1) из
|
||||||
`content/pack_odds.yaml`; duplicate protection по `printingId` с порогом 4.
|
`content/pack_odds.yaml`; duplicate protection по `printingId` с порогом 4.
|
||||||
|
|
@ -391,7 +395,7 @@ Solution — **`server/Vanguard.slnx`**, не `.sln`.
|
||||||
/Storage/ EF-хранилище, восстановление матчей
|
/Storage/ EF-хранилище, восстановление матчей
|
||||||
/Profiles/ интерфейс MMR
|
/Profiles/ интерфейс MMR
|
||||||
/Dockerfile
|
/Dockerfile
|
||||||
/server/tests/Vanguard.Rules.Tests 131 тест
|
/server/tests/Vanguard.Rules.Tests 132 теста
|
||||||
/server/tests/Vanguard.Server.Tests 45 тестов, включая E2E и внешние хранилища
|
/server/tests/Vanguard.Server.Tests 45 тестов, включая E2E и внешние хранилища
|
||||||
/content/sets карты
|
/content/sets карты
|
||||||
/content/trial-decks.yaml две триал-деки
|
/content/trial-decks.yaml две триал-деки
|
||||||
|
|
@ -729,7 +733,7 @@ Battle; Boot и общие сервисы остаются персистент
|
||||||
нельзя).
|
нельзя).
|
||||||
- `MetaService.CraftAsync`; `GET /api/cards` отдаёт `power`, `shield`,
|
- `MetaService.CraftAsync`; `GET /api/cards` отдаёт `power`, `shield`,
|
||||||
`critical`, `type`, `textEn/textRu` и флаги способностей.
|
`critical`, `type`, `textEn/textRu` и флаги способностей.
|
||||||
- Проверки: серверные 131 + 45 на `ssh home`; Unity EditMode 2/2,
|
- Проверки: серверные 132 + 45 на `ssh home`; Unity EditMode 2/2,
|
||||||
`SCENE_SMOKE success=True active=Login`; live `GET /api/cards` — 124
|
`SCENE_SMOKE success=True active=Login`; live `GET /api/cards` — 124
|
||||||
принта; live UI-проба: загрузка 50 карт, создание колоды,
|
принта; live UI-проба: загрузка 50 карт, создание колоды,
|
||||||
добавление/удаление, ПКМ-оверлей, текст способности и разбор карты;
|
добавление/удаление, ПКМ-оверлей, текст способности и разбор карты;
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
матчмейкинг, WebSocket-матч, история, реплеи.
|
матчмейкинг, WebSocket-матч, история, реплеи.
|
||||||
- Orleans включается `Orleans:Enabled`; в compose включён. Один silo,
|
- Orleans включается `Orleans:Enabled`; в compose включён. Один silo,
|
||||||
in-process реестр соединений.
|
in-process реестр соединений.
|
||||||
- Тесты: **131** правил + **45** серверных, CI зелёный.
|
- Тесты: **132** правил + **45** серверных, CI зелёный.
|
||||||
- Стенд: `http://192.168.31.158:18080`, `/health` → ok.
|
- Стенд: `http://192.168.31.158:18080`, `/health` → ok.
|
||||||
- Документация: `docs/server-api.md`, `docs/protocol.md`,
|
- Документация: `docs/server-api.md`, `docs/protocol.md`,
|
||||||
`docs/HANDOFF.md`.
|
`docs/HANDOFF.md`.
|
||||||
|
|
|
||||||
|
|
@ -1289,6 +1289,15 @@ public sealed class GameEngine
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<GameCommand> LegalCommands()
|
public IReadOnlyList<GameCommand> LegalCommands()
|
||||||
{
|
{
|
||||||
|
// Защита от состояния «бой уже начался, но очередь эффектов оставила
|
||||||
|
// continuation невыполненным». Иначе клиент получает EndTurn,
|
||||||
|
// который движок затем отклоняет.
|
||||||
|
if (State.Step == Step.None && State.Battle is not null
|
||||||
|
&& State.Continuation is not null)
|
||||||
|
{
|
||||||
|
ContinueOrder();
|
||||||
|
}
|
||||||
|
|
||||||
var commands = new List<GameCommand>();
|
var commands = new List<GameCommand>();
|
||||||
if (State.IsFinished)
|
if (State.IsFinished)
|
||||||
return commands;
|
return commands;
|
||||||
|
|
@ -1425,6 +1434,10 @@ public sealed class GameEngine
|
||||||
{
|
{
|
||||||
foreach (var attackerSlot in UnitSlots(player))
|
foreach (var attackerSlot in UnitSlots(player))
|
||||||
{
|
{
|
||||||
|
// Задний ряд не атакует; -1 — Вэнгард, 3/4 — передний ряд.
|
||||||
|
if (attackerSlot is 0 or 1 or 2)
|
||||||
|
continue;
|
||||||
|
|
||||||
var attacker = State.GetUnit(player, attackerSlot);
|
var attacker = State.GetUnit(player, attackerSlot);
|
||||||
if (attacker is null || attacker.Rested)
|
if (attacker is null || attacker.Rested)
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -1857,6 +1870,11 @@ public sealed class GameEngine
|
||||||
if (State.HasRestriction(attacker, RestrictionType.CannotAttack))
|
if (State.HasRestriction(attacker, RestrictionType.CannotAttack))
|
||||||
return CommandResult.Reject("Юнит не может атаковать.");
|
return CommandResult.Reject("Юнит не может атаковать.");
|
||||||
|
|
||||||
|
// Атаковать могут только Вэнгард и Арьергард переднего ряда.
|
||||||
|
// Задний ряд — исключительно поддержка (boost) и эффекты.
|
||||||
|
if (attackerSlot is 0 or 1 or 2)
|
||||||
|
return CommandResult.Reject("Атаковать могут только юниты переднего ряда.");
|
||||||
|
|
||||||
var target = State.GetUnit(1 - State.ActivePlayer, targetSlot);
|
var target = State.GetUnit(1 - State.ActivePlayer, targetSlot);
|
||||||
if (target is null)
|
if (target is null)
|
||||||
return CommandResult.Reject("Нет цели.");
|
return CommandResult.Reject("Нет цели.");
|
||||||
|
|
|
||||||
|
|
@ -1707,6 +1707,7 @@ public class GameEngineTests
|
||||||
engine.Start();
|
engine.Start();
|
||||||
var random = new Random(seed);
|
var random = new Random(seed);
|
||||||
GameCommand? lastCommand = null;
|
GameCommand? lastCommand = null;
|
||||||
|
var history = new List<GameCommand>();
|
||||||
|
|
||||||
for (var step = 0; step < 3000 && !engine.State.IsFinished; step++)
|
for (var step = 0; step < 3000 && !engine.State.IsFinished; step++)
|
||||||
{
|
{
|
||||||
|
|
@ -1719,9 +1720,20 @@ public class GameEngineTests
|
||||||
+ $"выбора {engine.State.PendingChoice is not null}");
|
+ $"выбора {engine.State.PendingChoice is not null}");
|
||||||
|
|
||||||
var command = legal[random.Next(legal.Count)];
|
var command = legal[random.Next(legal.Count)];
|
||||||
|
history.Add(command);
|
||||||
|
if (history.Count > 6)
|
||||||
|
history.RemoveAt(0);
|
||||||
lastCommand = command;
|
lastCommand = command;
|
||||||
var result = engine.Execute(command);
|
var result = engine.Execute(command);
|
||||||
Assert.True(result.Accepted, $"{command} -> {result.Reason}");
|
Assert.True(result.Accepted, $"{seed}/{step}: {command} -> {result.Reason}; "
|
||||||
|
+ $"phase {engine.State.Phase}, step {engine.State.Step}, "
|
||||||
|
+ $"active {engine.State.ActivePlayer}, battle {engine.State.Battle is not null}, "
|
||||||
|
+ $"pending trigger {engine.State.PendingTrigger is not null}, "
|
||||||
|
+ $"ability {engine.State.PendingAbility is not null}, "
|
||||||
|
+ $"choice {engine.State.PendingChoice is not null}, "
|
||||||
|
+ $"queue {engine.State.PendingQueue?.Count ?? 0}, "
|
||||||
|
+ $"continuation {engine.State.Continuation is not null}; "
|
||||||
|
+ $"history {string.Join(" -> ", history)}");
|
||||||
|
|
||||||
AssertInvariants(engine.State, $"{seed}/{step}: {lastCommand}");
|
AssertInvariants(engine.State, $"{seed}/{step}: {lastCommand}");
|
||||||
}
|
}
|
||||||
|
|
@ -1730,6 +1742,33 @@ public class GameEngineTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Back_Row_RearGuard_Cannot_Attack()
|
||||||
|
{
|
||||||
|
var engine = InMainPhase();
|
||||||
|
Give(engine, 0, Marron);
|
||||||
|
Assert.True(engine.Execute(new CallCommand(Marron, 0)).Accepted);
|
||||||
|
|
||||||
|
Give(engine, 0, Gallatin);
|
||||||
|
Assert.True(engine.Execute(new CallCommand(Gallatin, 3)).Accepted);
|
||||||
|
|
||||||
|
engine.State.Players[0].RearGuards[0]!.Rested = false;
|
||||||
|
engine.State.Players[0].RearGuards[3]!.Rested = false;
|
||||||
|
|
||||||
|
Assert.True(engine.Execute(new EnterBattleCommand()).Accepted);
|
||||||
|
|
||||||
|
var back = engine.Execute(new AttackCommand(0, -1));
|
||||||
|
Assert.False(back.Accepted);
|
||||||
|
Assert.Contains("переднего ряда", back.Reason);
|
||||||
|
|
||||||
|
var legal = engine.LegalCommands();
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
legal,
|
||||||
|
command => command is AttackCommand attack && attack.AttackerSlot == 0);
|
||||||
|
|
||||||
|
Assert.True(engine.Execute(new AttackCommand(3, -1)).Accepted);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Finish_Awards_Winner_And_Rejects_Later_Commands()
|
public void Finish_Awards_Winner_And_Rejects_Later_Commands()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue