diff --git a/IMPLEMENTATION_CHECKLIST.md b/IMPLEMENTATION_CHECKLIST.md
index b9dda45..99b01cd 100644
--- a/IMPLEMENTATION_CHECKLIST.md
+++ b/IMPLEMENTATION_CHECKLIST.md
@@ -112,7 +112,7 @@
- [x] Перехват: карта 2 ранга из переднего ряда выходит в круг защиты, сила атаки сравнивается с ней; `InterceptOverride` и `InterceptFromBackRow` позволяют выдать способность эффектом.
- [x] Перемещение Арьергарда в пределах колонки; если круг занят — карты меняются местами; у центрального заднего ряда пары нет.
- [ ] Discard, Shuffle как отдельные интенты.
-- [ ] `driveCheckCount` как модифицируемое свойство (сейчас константа 2).
+- [x] Число проверок принадлежит атакующей карте: 0–2 ранг — одна, 3+ — две; Арьергард по умолчанию без проверок; `DriveChecksOverride` меняет число (в том числе снимает двойную).
- [x] Результат боя: `Hit` / `Guarded` как событие.
- [ ] Событийная шина и три обработчика: Continuous / Activated / Triggered.
- [ ] Check Timing: сбор → сортировка → разрешение по одному → повтор.
diff --git a/server/src/Vanguard.Rules/Cards/CardDefinition.cs b/server/src/Vanguard.Rules/Cards/CardDefinition.cs
index 965d85f..86e2420 100644
--- a/server/src/Vanguard.Rules/Cards/CardDefinition.cs
+++ b/server/src/Vanguard.Rules/Cards/CardDefinition.cs
@@ -40,6 +40,12 @@ public sealed record CardDefinition
/// Печатный перехват: по умолчанию у карт 2 ранга.
public bool HasIntercept => InterceptOverride ?? Grade == 2;
+ ///
+ /// Печатное число проверок при атаке из круга Авангарда:
+ /// 0–2 ранг — одна, 3 ранг и выше — двойная.
+ ///
+ public int DefaultDriveChecks => Grade >= 3 ? 2 : 1;
+
public string? TextEn { get; init; }
public string? TextRu { get; init; }
diff --git a/server/src/Vanguard.Rules/Engine/GameEngine.cs b/server/src/Vanguard.Rules/Engine/GameEngine.cs
index ced8234..b529f73 100644
--- a/server/src/Vanguard.Rules/Engine/GameEngine.cs
+++ b/server/src/Vanguard.Rules/Engine/GameEngine.cs
@@ -15,6 +15,8 @@ namespace Vanguard.Rules.Engine;
/// в круг защиты и при атаке по Авангарду, и при атаке по Арьергарду;
/// атакованный Арьергард не может перехватить атаку на самого себя;
/// из заднего ряда — только если эффект разрешил (InterceptFromBackRow);
+/// * число проверок принадлежит атакующей карте: 0–2 ранг — одна, 3+ — две;
+/// Арьергард по умолчанию без проверок, но эффект может их выдать;
/// * триггер требует выбора целей через ResolveTriggerCommand:
/// +5000 любой карте в кругах Авангарда или Арьергарда, затем второй эффект;
/// правило одинаково для drive check и damage check;
@@ -25,7 +27,6 @@ public sealed class GameEngine
{
public const int StartingHand = 5;
public const int MaxDamage = 6;
- public const int DriveChecks = 2;
public const int TriggerPowerBonus = 5000;
private readonly List _events = new();
@@ -48,6 +49,18 @@ public sealed class GameEngine
public GameState State { get; }
public IReadOnlyList Events => _events;
+ ///
+ /// Сколько проверок делает атакующая карта. Эффект может изменить число;
+ /// Арьергард по умолчанию проверок не делает.
+ ///
+ public int DriveChecksFor(int slot, CardInstance attacker)
+ {
+ if (attacker.DriveChecksOverride is { } overridden)
+ return Math.Max(0, overridden);
+
+ return slot < 0 ? State.Catalog.CardOf(attacker.PrintingId).DefaultDriveChecks : 0;
+ }
+
// ---------- старт партии ----------
public void Start()
@@ -516,13 +529,21 @@ public sealed class GameEngine
private void BeginResolution(BattleState battle)
{
- if (battle.AttackerSlot >= 0)
+ var attacker = State.GetUnit(battle.AttackerPlayer, battle.AttackerSlot);
+ if (attacker is null)
{
FinishBattle(battle);
return;
}
- battle.ChecksRemaining = DriveChecks;
+ var checks = DriveChecksFor(battle.AttackerSlot, attacker);
+ if (checks == 0)
+ {
+ FinishBattle(battle);
+ return;
+ }
+
+ battle.ChecksRemaining = checks;
State.ActivePlayer = battle.AttackerPlayer;
State.Step = Step.Drive;
ContinueDriveChecks(battle);
@@ -546,6 +567,8 @@ public sealed class GameEngine
}
var def = State.Catalog.CardOf(card.PrintingId);
+ _events.Add(new DriveCheckedEvent(player, card.PrintingId, def.Trigger));
+
if (def.Trigger == TriggerType.None)
{
State.Players[player].Hand.Add(card);
diff --git a/server/src/Vanguard.Rules/Engine/GameEvents.cs b/server/src/Vanguard.Rules/Engine/GameEvents.cs
index 77ebf33..55a5485 100644
--- a/server/src/Vanguard.Rules/Engine/GameEvents.cs
+++ b/server/src/Vanguard.Rules/Engine/GameEvents.cs
@@ -11,6 +11,7 @@ public sealed record CardDrawnEvent(int Player) : GameEvent;
public sealed record UnitRodeEvent(int Player, string PrintingId) : GameEvent;
public sealed record UnitCalledEvent(int Player, int Slot, string PrintingId) : GameEvent;
public sealed record UnitMovedEvent(int Player, int FromSlot, int ToSlot) : GameEvent;
+public sealed record DriveCheckedEvent(int Player, string PrintingId, TriggerType Trigger) : GameEvent;
public sealed record AttackDeclaredEvent(int Player, int AttackerSlot, int TargetSlot) : GameEvent;
public sealed record BoostedEvent(int Player, int BoosterSlot, int AttackerSlot) : GameEvent;
public sealed record GuardAddedEvent(int Player, string PrintingId, int Shield) : GameEvent;
diff --git a/server/src/Vanguard.Rules/Engine/GameModels.cs b/server/src/Vanguard.Rules/Engine/GameModels.cs
index 890b705..b104761 100644
--- a/server/src/Vanguard.Rules/Engine/GameModels.cs
+++ b/server/src/Vanguard.Rules/Engine/GameModels.cs
@@ -72,6 +72,12 @@ public sealed class CardInstance
/// Эффект разрешил перехватывать из заднего ряда.
public bool InterceptFromBackRow { get; set; }
+
+ ///
+ /// Переопределение числа проверок, выданное эффектом.
+ /// null — по умолчанию: 1 для 0–2 ранга, 2 для 3+, у Арьергарда 0.
+ ///
+ public int? DriveChecksOverride { get; set; }
}
public sealed class PlayerState
diff --git a/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs b/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs
index b688776..1a94ad5 100644
--- a/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs
+++ b/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs
@@ -472,6 +472,73 @@ public class GameEngineTests
Assert.False(engine.Execute(new MoveCommand(0, 3)).Accepted);
}
+ [Fact]
+ public void Grade_One_Vanguard_Performs_One_Check()
+ {
+ var engine = ReadyToAttack();
+ StackDeck(engine.State.Players[0], Gallatin, Gallatin);
+
+ engine.Execute(new AttackCommand(-1, -1));
+ engine.Execute(new PassGuardCommand());
+
+ Assert.Single(engine.Events.OfType());
+ }
+
+ [Fact]
+ public void Grade_Three_Vanguard_Performs_Two_Checks()
+ {
+ var engine = StartedEngine();
+ engine.Execute(new SkipRideCommand());
+ SetVanguard(engine, 0, Overlord);
+ StackDeck(engine.State.Players[0], Gallatin, Gallatin);
+ engine.Execute(new EnterBattleCommand());
+
+ engine.Execute(new AttackCommand(-1, -1));
+ engine.Execute(new PassGuardCommand());
+
+ Assert.Equal(2, engine.Events.OfType().Count());
+ }
+
+ [Fact]
+ public void Twin_Drive_Can_Be_Removed_By_Effect()
+ {
+ var engine = StartedEngine();
+ engine.Execute(new SkipRideCommand());
+ SetVanguard(engine, 0, Overlord);
+ engine.State.Players[0].Vanguard!.DriveChecksOverride = 1;
+ StackDeck(engine.State.Players[0], Gallatin, Gallatin);
+ engine.Execute(new EnterBattleCommand());
+
+ engine.Execute(new AttackCommand(-1, -1));
+ engine.Execute(new PassGuardCommand());
+
+ Assert.Single(engine.Events.OfType());
+ }
+
+ [Fact]
+ public void Rear_Guard_Performs_No_Checks_By_Default()
+ {
+ var engine = ReadyToAttackRearGuard();
+
+ engine.Execute(new AttackCommand(3, 0));
+ engine.Execute(new PassGuardCommand());
+
+ Assert.Empty(engine.Events.OfType());
+ }
+
+ [Fact]
+ public void Effect_Can_Give_Drive_Check_To_Rear_Guard()
+ {
+ var engine = ReadyToAttackRearGuard();
+ engine.State.Players[0].RearGuards[3]!.DriveChecksOverride = 1;
+ StackDeck(engine.State.Players[0], Gallatin);
+
+ engine.Execute(new AttackCommand(3, 0));
+ engine.Execute(new PassGuardCommand());
+
+ Assert.Single(engine.Events.OfType());
+ }
+
[Fact]
public void ResolveTrigger_Rejects_When_Nothing_Is_Pending()
{