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(
|
||||
grid,
|
||||
card,
|
||||
slot == -1 ? "V" : "R" + (slot + 1),
|
||||
slot == -1 ? "V" : slot <= 2 ? "SUP" + (slot + 1) : "R" + (slot + 1),
|
||||
compact: false,
|
||||
player.Index,
|
||||
slot);
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ MVP: бустер **BT01** + триал-деки **TD01/TD02** — 94 карты
|
|||
|
||||
| Проверка | Результат |
|
||||
|---|---|
|
||||
| Тесты сервера | **176/176** (131 правила/контракты + 45 сервер) |
|
||||
| Тесты сервера | **177/177** (132 правила/контракты + 45 сервер) |
|
||||
| `validate` | 0 ошибок, 0 предупреждений |
|
||||
| `translations check` | 94/94 |
|
||||
| `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)
|
||||
OK: ошибок 0, предупреждений 0
|
||||
Переведено: 94/94
|
||||
|
|
@ -305,7 +305,7 @@ Solution — **`server/Vanguard.slnx`**, не `.sln`.
|
|||
Orleans сериализуется (тестовый `TestCluster`); полный матч двух
|
||||
клиентов через HTTP/WebSocket при `Orleans:Enabled=true`.
|
||||
|
||||
Проверено на `ssh home`: 131 + 45 теста, включая реальные PostgreSQL и Redis;
|
||||
Проверено на `ssh home`: 132 + 45 теста, включая реальные PostgreSQL и Redis;
|
||||
`docker compose up` поднимает стенд, матч создаётся через HTTP-очередь,
|
||||
а после `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 нельзя.
|
||||
- **Колоды**: CRUD, валидация 50 карт / ≤4 по `nameId` / Sentinel / Heal /
|
||||
стартовый Авангард, проверка владения картами, лимит 30, шаринг по коду.
|
||||
- **Правило атаки**: атакуют только Вэнгард и Арьергард переднего ряда
|
||||
(слоты 3/4); задний ряд (0–2) — только поддержка/boost. Задний ряд
|
||||
также нельзя атаковать без `snipeBackRow`. `LegalCommands` не предлагает
|
||||
атаки из заднего ряда; добавлен регрессионный тест.
|
||||
- **Магазин**: бустер 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.
|
||||
|
|
@ -391,7 +395,7 @@ Solution — **`server/Vanguard.slnx`**, не `.sln`.
|
|||
/Storage/ EF-хранилище, восстановление матчей
|
||||
/Profiles/ интерфейс MMR
|
||||
/Dockerfile
|
||||
/server/tests/Vanguard.Rules.Tests 131 тест
|
||||
/server/tests/Vanguard.Rules.Tests 132 теста
|
||||
/server/tests/Vanguard.Server.Tests 45 тестов, включая E2E и внешние хранилища
|
||||
/content/sets карты
|
||||
/content/trial-decks.yaml две триал-деки
|
||||
|
|
@ -729,7 +733,7 @@ Battle; Boot и общие сервисы остаются персистент
|
|||
нельзя).
|
||||
- `MetaService.CraftAsync`; `GET /api/cards` отдаёт `power`, `shield`,
|
||||
`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
|
||||
принта; live UI-проба: загрузка 50 карт, создание колоды,
|
||||
добавление/удаление, ПКМ-оверлей, текст способности и разбор карты;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
матчмейкинг, WebSocket-матч, история, реплеи.
|
||||
- Orleans включается `Orleans:Enabled`; в compose включён. Один silo,
|
||||
in-process реестр соединений.
|
||||
- Тесты: **131** правил + **45** серверных, CI зелёный.
|
||||
- Тесты: **132** правил + **45** серверных, CI зелёный.
|
||||
- Стенд: `http://192.168.31.158:18080`, `/health` → ok.
|
||||
- Документация: `docs/server-api.md`, `docs/protocol.md`,
|
||||
`docs/HANDOFF.md`.
|
||||
|
|
|
|||
|
|
@ -1289,6 +1289,15 @@ public sealed class GameEngine
|
|||
/// </summary>
|
||||
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>();
|
||||
if (State.IsFinished)
|
||||
return commands;
|
||||
|
|
@ -1425,6 +1434,10 @@ public sealed class GameEngine
|
|||
{
|
||||
foreach (var attackerSlot in UnitSlots(player))
|
||||
{
|
||||
// Задний ряд не атакует; -1 — Вэнгард, 3/4 — передний ряд.
|
||||
if (attackerSlot is 0 or 1 or 2)
|
||||
continue;
|
||||
|
||||
var attacker = State.GetUnit(player, attackerSlot);
|
||||
if (attacker is null || attacker.Rested)
|
||||
continue;
|
||||
|
|
@ -1857,6 +1870,11 @@ public sealed class GameEngine
|
|||
if (State.HasRestriction(attacker, RestrictionType.CannotAttack))
|
||||
return CommandResult.Reject("Юнит не может атаковать.");
|
||||
|
||||
// Атаковать могут только Вэнгард и Арьергард переднего ряда.
|
||||
// Задний ряд — исключительно поддержка (boost) и эффекты.
|
||||
if (attackerSlot is 0 or 1 or 2)
|
||||
return CommandResult.Reject("Атаковать могут только юниты переднего ряда.");
|
||||
|
||||
var target = State.GetUnit(1 - State.ActivePlayer, targetSlot);
|
||||
if (target is null)
|
||||
return CommandResult.Reject("Нет цели.");
|
||||
|
|
|
|||
|
|
@ -1707,6 +1707,7 @@ public class GameEngineTests
|
|||
engine.Start();
|
||||
var random = new Random(seed);
|
||||
GameCommand? lastCommand = null;
|
||||
var history = new List<GameCommand>();
|
||||
|
||||
for (var step = 0; step < 3000 && !engine.State.IsFinished; step++)
|
||||
{
|
||||
|
|
@ -1719,9 +1720,20 @@ public class GameEngineTests
|
|||
+ $"выбора {engine.State.PendingChoice is not null}");
|
||||
|
||||
var command = legal[random.Next(legal.Count)];
|
||||
history.Add(command);
|
||||
if (history.Count > 6)
|
||||
history.RemoveAt(0);
|
||||
lastCommand = 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}");
|
||||
}
|
||||
|
|
@ -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]
|
||||
public void Finish_Awards_Winner_And_Rejects_Later_Commands()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue