Trigger targeting: +5000 and secondary effect are chosen by the player
Drive check now pauses on a trigger and waits for ResolveTriggerCommand(powerTarget, effectTarget). Power and crit modifiers moved from the battle to the unit and are cleared at the end of the battle. Stand stands the chosen rear-guard; Critical applies +1 crit to the chosen unit. Damage-check triggers still apply only the secondary effect.
This commit is contained in:
parent
1b317b6532
commit
a4457c6d21
|
|
@ -114,7 +114,7 @@
|
||||||
- [ ] Check Timing: сбор → сортировка → разрешение по одному → повтор.
|
- [ ] Check Timing: сбор → сортировка → разрешение по одному → повтор.
|
||||||
- [ ] Стоимости: CB, SB, discard, rest self, retire self, put to soul, return to deck.
|
- [ ] Стоимости: CB, SB, discard, rest self, retire self, put to soul, return to deck.
|
||||||
- [ ] Цели: авто по фильтру и выбор игроком, «до N» / «ровно N».
|
- [ ] Цели: авто по фильтру и выбор игроком, «до N» / «ровно N».
|
||||||
- [ ] Слои модификаторов силы/крита с длительностью.
|
- [~] Слои модификаторов силы/крита: временные бонусы до конца боя есть, постоянных слоёв нет.
|
||||||
- [x] Валидация интентов: `Rejected(reason)` без изменения состояния.
|
- [x] Валидация интентов: `Rejected(reason)` без изменения состояния.
|
||||||
- [ ] Список легальных интентов на каждый момент (для UI и античита).
|
- [ ] Список легальных интентов на каждый момент (для UI и античита).
|
||||||
- [~] Детерминированный RNG: один поток на партию, состояние в `GameState`.
|
- [~] Детерминированный RNG: один поток на партию, состояние в `GameState`.
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ public sealed record EnterBattleCommand : GameCommand;
|
||||||
public sealed record AttackCommand(int AttackerSlot, int TargetSlot) : GameCommand;
|
public sealed record AttackCommand(int AttackerSlot, int TargetSlot) : GameCommand;
|
||||||
public sealed record GuardCommand(string PrintingId) : GameCommand;
|
public sealed record GuardCommand(string PrintingId) : GameCommand;
|
||||||
public sealed record PassGuardCommand : GameCommand;
|
public sealed record PassGuardCommand : GameCommand;
|
||||||
|
public sealed record ResolveTriggerCommand(int PowerTarget, int EffectTarget) : GameCommand;
|
||||||
public sealed record EndTurnCommand : GameCommand;
|
public sealed record EndTurnCommand : GameCommand;
|
||||||
|
|
||||||
public sealed record CommandResult(bool Accepted, string? Reason)
|
public sealed record CommandResult(bool Accepted, string? Reason)
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,10 @@ namespace Vanguard.Rules.Engine;
|
||||||
/// Упрощения первого среза (карты со способностями подключаются следующим шагом):
|
/// Упрощения первого среза (карты со способностями подключаются следующим шагом):
|
||||||
/// * Sentinel добавляет 0 щита и не оплачивает стоимость — Perfect Guard пока не работает;
|
/// * Sentinel добавляет 0 щита и не оплачивает стоимость — Perfect Guard пока не работает;
|
||||||
/// * Boost не реализован;
|
/// * Boost не реализован;
|
||||||
/// * выбор цели триггера автоматический: усиление получает атакующая карта,
|
/// * триггер на drive check требует выбора целей через ResolveTriggerCommand:
|
||||||
/// Stand возвращает первый повёрнутый Арьергард.
|
/// +5000 любой карте в кругах Авангарда или Арьергарда, затем второй эффект;
|
||||||
|
/// * триггер на damage check пока применяет только второй эффект,
|
||||||
|
/// Stand поднимает первый повёрнутый Арьергард.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GameEngine
|
public sealed class GameEngine
|
||||||
{
|
{
|
||||||
|
|
@ -105,6 +107,7 @@ public sealed class GameEngine
|
||||||
AttackCommand c => Attack(c.AttackerSlot, c.TargetSlot),
|
AttackCommand c => Attack(c.AttackerSlot, c.TargetSlot),
|
||||||
GuardCommand c => Guard(c.PrintingId),
|
GuardCommand c => Guard(c.PrintingId),
|
||||||
PassGuardCommand => PassGuard(),
|
PassGuardCommand => PassGuard(),
|
||||||
|
ResolveTriggerCommand c => ResolveTrigger(c),
|
||||||
EndTurnCommand => EndTurn(),
|
EndTurnCommand => EndTurn(),
|
||||||
_ => CommandResult.Reject("Неизвестная команда."),
|
_ => CommandResult.Reject("Неизвестная команда."),
|
||||||
};
|
};
|
||||||
|
|
@ -307,7 +310,7 @@ public sealed class GameEngine
|
||||||
if (State.ActivePlayer != 1 - battle.AttackerPlayer)
|
if (State.ActivePlayer != 1 - battle.AttackerPlayer)
|
||||||
return CommandResult.Reject("Защищается только обороняющийся.");
|
return CommandResult.Reject("Защищается только обороняющийся.");
|
||||||
|
|
||||||
ResolveBattle(battle);
|
BeginResolution(battle);
|
||||||
return CommandResult.Ok();
|
return CommandResult.Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -368,8 +371,133 @@ public sealed class GameEngine
|
||||||
|
|
||||||
// ---------- бой ----------
|
// ---------- бой ----------
|
||||||
|
|
||||||
private void ResolveBattle(BattleState battle)
|
private void BeginResolution(BattleState battle)
|
||||||
{
|
{
|
||||||
|
if (battle.AttackerSlot >= 0)
|
||||||
|
{
|
||||||
|
FinishBattle(battle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
battle.ChecksRemaining = DriveChecks;
|
||||||
|
State.ActivePlayer = battle.AttackerPlayer;
|
||||||
|
State.Step = Step.Drive;
|
||||||
|
ContinueDriveChecks(battle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Идёт по drive-чекам. На триггере останавливается и ждёт ResolveTriggerCommand:
|
||||||
|
/// игрок сам выбирает, кому дать +5000 и куда применить второй эффект.
|
||||||
|
/// </summary>
|
||||||
|
private void ContinueDriveChecks(BattleState battle)
|
||||||
|
{
|
||||||
|
var player = battle.AttackerPlayer;
|
||||||
|
|
||||||
|
while (battle.ChecksRemaining > 0 && !State.IsFinished)
|
||||||
|
{
|
||||||
|
var card = TakeTop(player);
|
||||||
|
if (card is null)
|
||||||
|
{
|
||||||
|
FinishBattle(battle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var def = State.Catalog.CardOf(card.PrintingId);
|
||||||
|
if (def.Trigger == TriggerType.None)
|
||||||
|
{
|
||||||
|
State.Players[player].Hand.Add(card);
|
||||||
|
_events.Add(new CardDrawnEvent(player));
|
||||||
|
battle.ChecksRemaining--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
State.Players[player].Trigger.Add(card);
|
||||||
|
battle.ChecksRemaining--;
|
||||||
|
_events.Add(new TriggerResolvedEvent(player, card.PrintingId, def.Trigger));
|
||||||
|
|
||||||
|
State.PendingTrigger = new PendingTrigger
|
||||||
|
{
|
||||||
|
Player = player,
|
||||||
|
PrintingId = card.PrintingId,
|
||||||
|
Trigger = def.Trigger,
|
||||||
|
Context = TriggerContext.DriveCheck,
|
||||||
|
GrantsPower = true,
|
||||||
|
};
|
||||||
|
State.Step = Step.Trigger;
|
||||||
|
State.ActivePlayer = player;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FinishBattle(battle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CommandResult ResolveTrigger(ResolveTriggerCommand command)
|
||||||
|
{
|
||||||
|
var pending = State.PendingTrigger;
|
||||||
|
if (pending is null || State.Step != Step.Trigger)
|
||||||
|
return CommandResult.Reject("Сейчас нет триггера для разрешения.");
|
||||||
|
|
||||||
|
if (State.ActivePlayer != pending.Player)
|
||||||
|
return CommandResult.Reject("Триггер разрешает его владелец.");
|
||||||
|
|
||||||
|
var player = pending.Player;
|
||||||
|
|
||||||
|
if (pending.GrantsPower)
|
||||||
|
{
|
||||||
|
var powerTarget = State.GetUnit(player, command.PowerTarget);
|
||||||
|
if (powerTarget is null)
|
||||||
|
return CommandResult.Reject("Нет цели для +5000 силы.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pending.Trigger is TriggerType.Critical or TriggerType.Stand)
|
||||||
|
{
|
||||||
|
var effectUnit = State.GetUnit(player, command.EffectTarget);
|
||||||
|
if (effectUnit is null)
|
||||||
|
return CommandResult.Reject("Нет цели для второго эффекта.");
|
||||||
|
|
||||||
|
if (pending.Trigger == TriggerType.Stand && command.EffectTarget < 0)
|
||||||
|
return CommandResult.Reject("Поднять можно только карту в кругу Арьергарда.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pending.GrantsPower)
|
||||||
|
State.GetUnit(player, command.PowerTarget)!.TemporaryPower += TriggerPowerBonus;
|
||||||
|
|
||||||
|
switch (pending.Trigger)
|
||||||
|
{
|
||||||
|
case TriggerType.Critical:
|
||||||
|
State.GetUnit(player, command.EffectTarget)!.TemporaryCritical += 1;
|
||||||
|
break;
|
||||||
|
case TriggerType.Stand:
|
||||||
|
State.GetUnit(player, command.EffectTarget)!.Rested = false;
|
||||||
|
break;
|
||||||
|
case TriggerType.Draw:
|
||||||
|
Draw(player, 1);
|
||||||
|
break;
|
||||||
|
case TriggerType.Heal:
|
||||||
|
Heal(player);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
State.PendingTrigger = null;
|
||||||
|
_events.Add(new TriggerAppliedEvent(player, command.PowerTarget, command.EffectTarget));
|
||||||
|
|
||||||
|
if (pending.Context == TriggerContext.DriveCheck && State.Battle is { } battle)
|
||||||
|
{
|
||||||
|
State.Step = Step.Drive;
|
||||||
|
ContinueDriveChecks(battle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CommandResult.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FinishBattle(BattleState battle)
|
||||||
|
{
|
||||||
|
if (State.IsFinished)
|
||||||
|
{
|
||||||
|
ClearBattle(battle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var defenderIndex = 1 - battle.AttackerPlayer;
|
var defenderIndex = 1 - battle.AttackerPlayer;
|
||||||
var attacker = State.GetUnit(battle.AttackerPlayer, battle.AttackerSlot);
|
var attacker = State.GetUnit(battle.AttackerPlayer, battle.AttackerSlot);
|
||||||
var target = State.GetUnit(defenderIndex, battle.TargetSlot);
|
var target = State.GetUnit(defenderIndex, battle.TargetSlot);
|
||||||
|
|
@ -380,20 +508,11 @@ public sealed class GameEngine
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (battle.AttackerSlot < 0)
|
|
||||||
DriveCheck(battle);
|
|
||||||
|
|
||||||
if (State.IsFinished)
|
|
||||||
{
|
|
||||||
ClearBattle(battle);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var attackerDef = State.Catalog.CardOf(attacker.PrintingId);
|
var attackerDef = State.Catalog.CardOf(attacker.PrintingId);
|
||||||
var targetDef = State.Catalog.CardOf(target.PrintingId);
|
var targetDef = State.Catalog.CardOf(target.PrintingId);
|
||||||
|
|
||||||
var attackPower = attackerDef.Power + battle.PowerBonus;
|
var attackPower = attackerDef.Power + attacker.TemporaryPower;
|
||||||
var defensePower = targetDef.Power + battle.GuardShield;
|
var defensePower = targetDef.Power + target.TemporaryPower + battle.GuardShield;
|
||||||
var hit = attackPower > defensePower;
|
var hit = attackPower > defensePower;
|
||||||
|
|
||||||
_events.Add(new BattleResolvedEvent(hit));
|
_events.Add(new BattleResolvedEvent(hit));
|
||||||
|
|
@ -402,7 +521,7 @@ public sealed class GameEngine
|
||||||
{
|
{
|
||||||
if (battle.TargetSlot < 0)
|
if (battle.TargetSlot < 0)
|
||||||
{
|
{
|
||||||
var damage = attackerDef.Critical + battle.CriticalBonus;
|
var damage = attackerDef.Critical + attacker.TemporaryCritical;
|
||||||
DealDamage(defenderIndex, damage);
|
DealDamage(defenderIndex, damage);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -425,54 +544,18 @@ public sealed class GameEngine
|
||||||
defender.Drop.Add(guard);
|
defender.Drop.Add(guard);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var unit in AllUnits(0).Concat(AllUnits(1)))
|
||||||
|
{
|
||||||
|
unit.TemporaryPower = 0;
|
||||||
|
unit.TemporaryCritical = 0;
|
||||||
|
}
|
||||||
|
|
||||||
State.Battle = null;
|
State.Battle = null;
|
||||||
|
State.PendingTrigger = null;
|
||||||
State.Step = Step.None;
|
State.Step = Step.None;
|
||||||
State.ActivePlayer = battle.AttackerPlayer;
|
State.ActivePlayer = battle.AttackerPlayer;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DriveCheck(BattleState battle)
|
|
||||||
{
|
|
||||||
var player = battle.AttackerPlayer;
|
|
||||||
|
|
||||||
for (var i = 0; i < DriveChecks; i++)
|
|
||||||
{
|
|
||||||
if (State.IsFinished)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var card = TakeTop(player);
|
|
||||||
if (card is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var def = State.Catalog.CardOf(card.PrintingId);
|
|
||||||
if (def.Trigger == TriggerType.None)
|
|
||||||
{
|
|
||||||
State.Players[player].Hand.Add(card);
|
|
||||||
_events.Add(new CardDrawnEvent(player));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
State.Players[player].Trigger.Add(card);
|
|
||||||
battle.PowerBonus += TriggerPowerBonus;
|
|
||||||
_events.Add(new TriggerResolvedEvent(player, card.PrintingId, def.Trigger));
|
|
||||||
|
|
||||||
switch (def.Trigger)
|
|
||||||
{
|
|
||||||
case TriggerType.Critical:
|
|
||||||
battle.CriticalBonus += 1;
|
|
||||||
break;
|
|
||||||
case TriggerType.Draw:
|
|
||||||
Draw(player, 1);
|
|
||||||
break;
|
|
||||||
case TriggerType.Stand:
|
|
||||||
StandFirstRested(player);
|
|
||||||
break;
|
|
||||||
case TriggerType.Heal:
|
|
||||||
Heal(player);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DealDamage(int player, int amount)
|
private void DealDamage(int player, int amount)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < amount; i++)
|
for (var i = 0; i < amount; i++)
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ public sealed record UnitCalledEvent(int Player, int Slot, string PrintingId) :
|
||||||
public sealed record AttackDeclaredEvent(int Player, int AttackerSlot, int TargetSlot) : GameEvent;
|
public sealed record AttackDeclaredEvent(int Player, int AttackerSlot, int TargetSlot) : GameEvent;
|
||||||
public sealed record GuardAddedEvent(int Player, string PrintingId, int Shield) : GameEvent;
|
public sealed record GuardAddedEvent(int Player, string PrintingId, int Shield) : GameEvent;
|
||||||
public sealed record TriggerResolvedEvent(int Player, string PrintingId, TriggerType Trigger) : GameEvent;
|
public sealed record TriggerResolvedEvent(int Player, string PrintingId, TriggerType Trigger) : GameEvent;
|
||||||
|
public sealed record TriggerAppliedEvent(int Player, int PowerTarget, int EffectTarget) : GameEvent;
|
||||||
public sealed record BattleResolvedEvent(bool Hit) : GameEvent;
|
public sealed record BattleResolvedEvent(bool Hit) : GameEvent;
|
||||||
public sealed record DamageTakenEvent(int Player, int Amount, int Total) : GameEvent;
|
public sealed record DamageTakenEvent(int Player, int Amount, int Total) : GameEvent;
|
||||||
public sealed record HealedEvent(int Player) : GameEvent;
|
public sealed record HealedEvent(int Player) : GameEvent;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
using Vanguard.Rules.Cards;
|
||||||
|
|
||||||
namespace Vanguard.Rules.Engine;
|
namespace Vanguard.Rules.Engine;
|
||||||
|
|
||||||
public enum Phase
|
public enum Phase
|
||||||
|
|
@ -15,6 +17,26 @@ public enum Step
|
||||||
{
|
{
|
||||||
None,
|
None,
|
||||||
Guard,
|
Guard,
|
||||||
|
Drive,
|
||||||
|
Trigger,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum TriggerContext
|
||||||
|
{
|
||||||
|
DriveCheck,
|
||||||
|
DamageCheck,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Триггер, который ждёт выбора целей от владельца.</summary>
|
||||||
|
public sealed class PendingTrigger
|
||||||
|
{
|
||||||
|
public required int Player { get; init; }
|
||||||
|
public required string PrintingId { get; init; }
|
||||||
|
public required TriggerType Trigger { get; init; }
|
||||||
|
public required TriggerContext Context { get; init; }
|
||||||
|
|
||||||
|
/// <summary>На drive check триггер сначала даёт +5000 силы, на damage check — нет.</summary>
|
||||||
|
public required bool GrantsPower { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Экземпляр карты в партии. Один принт может встречаться много раз.</summary>
|
/// <summary>Экземпляр карты в партии. Один принт может встречаться много раз.</summary>
|
||||||
|
|
@ -28,6 +50,12 @@ public sealed class CardInstance
|
||||||
|
|
||||||
/// <summary>Открыта ли карта. Для урона — лицом вверх после Counter Blast.</summary>
|
/// <summary>Открыта ли карта. Для урона — лицом вверх после Counter Blast.</summary>
|
||||||
public bool FaceUp { get; set; }
|
public bool FaceUp { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Временный бонус силы до конца текущего боя (триггеры, эффекты).</summary>
|
||||||
|
public int TemporaryPower { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Временный бонус крита до конца текущего боя.</summary>
|
||||||
|
public int TemporaryCritical { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class PlayerState
|
public sealed class PlayerState
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,9 @@ public sealed class BattleState
|
||||||
/// <summary>Индекс цели: -1 — Авангард противника.</summary>
|
/// <summary>Индекс цели: -1 — Авангард противника.</summary>
|
||||||
public required int TargetSlot { get; init; }
|
public required int TargetSlot { get; init; }
|
||||||
|
|
||||||
public int PowerBonus { get; set; }
|
|
||||||
public int CriticalBonus { get; set; }
|
|
||||||
public List<CardInstance> GuardCards { get; } = new();
|
|
||||||
public int GuardShield { get; set; }
|
public int GuardShield { get; set; }
|
||||||
|
public int ChecksRemaining { get; set; }
|
||||||
|
public List<CardInstance> GuardCards { get; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class GameState
|
public sealed class GameState
|
||||||
|
|
@ -40,6 +39,7 @@ public sealed class GameState
|
||||||
public int NextInstanceId { get; internal set; } = 1;
|
public int NextInstanceId { get; internal set; } = 1;
|
||||||
|
|
||||||
public BattleState? Battle { get; internal set; }
|
public BattleState? Battle { get; internal set; }
|
||||||
|
public PendingTrigger? PendingTrigger { get; internal set; }
|
||||||
|
|
||||||
public bool IsFinished { get; internal set; }
|
public bool IsFinished { get; internal set; }
|
||||||
public int? Winner { get; internal set; }
|
public int? Winner { get; internal set; }
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@ public class GameEngineTests
|
||||||
private const string Marron = "BT01/042EN"; // grade 1, 8000
|
private const string Marron = "BT01/042EN"; // grade 1, 8000
|
||||||
private const string Gallatin = "BT01/021EN"; // grade 2, 10000
|
private const string Gallatin = "BT01/021EN"; // grade 2, 10000
|
||||||
private const string Alfred = "BT01/001EN"; // grade 3
|
private const string Alfred = "BT01/001EN"; // grade 3
|
||||||
|
private const string Nike = "BT01/056EN"; // Critical
|
||||||
|
private const string Flogal = "BT01/046EN"; // Stand
|
||||||
|
private const string Govannon = "BT01/045EN"; // Draw
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Start_Places_Starting_Vanguard_And_Draws_Five()
|
public void Start_Places_Starting_Vanguard_And_Draws_Five()
|
||||||
|
|
@ -115,6 +118,49 @@ public class GameEngineTests
|
||||||
Assert.Contains(engine.Events, e => e is GameFinishedEvent);
|
Assert.Contains(engine.Events, e => e is GameFinishedEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Critical_Trigger_Grants_Power_And_Crit_To_Chosen_Unit()
|
||||||
|
{
|
||||||
|
var engine = ReadyToAttack();
|
||||||
|
StackDeck(engine.State.Players[0], Nike, Gallatin);
|
||||||
|
|
||||||
|
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||||||
|
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||||||
|
|
||||||
|
Assert.NotNull(engine.State.PendingTrigger);
|
||||||
|
Assert.Equal(Step.Trigger, engine.State.Step);
|
||||||
|
|
||||||
|
Assert.True(engine.Execute(new ResolveTriggerCommand(-1, -1)).Accepted);
|
||||||
|
|
||||||
|
Assert.Null(engine.State.PendingTrigger);
|
||||||
|
Assert.Equal(2, engine.State.Players[1].Damage.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Stand_Trigger_Stands_Chosen_RearGuard()
|
||||||
|
{
|
||||||
|
var engine = ReadyToAttackWithRearGuard();
|
||||||
|
StackDeck(engine.State.Players[0], Flogal, Gallatin);
|
||||||
|
|
||||||
|
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||||||
|
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||||||
|
Assert.NotNull(engine.State.PendingTrigger);
|
||||||
|
|
||||||
|
var rearGuard = engine.State.Players[0].RearGuards[0]!;
|
||||||
|
Assert.True(rearGuard.Rested);
|
||||||
|
|
||||||
|
Assert.True(engine.Execute(new ResolveTriggerCommand(-1, 0)).Accepted);
|
||||||
|
Assert.False(rearGuard.Rested);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveTrigger_Rejects_When_Nothing_Is_Pending()
|
||||||
|
{
|
||||||
|
var engine = StartedEngine();
|
||||||
|
|
||||||
|
Assert.False(engine.Execute(new ResolveTriggerCommand(-1, -1)).Accepted);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Same_Seed_And_Commands_Produce_Same_Events()
|
public void Same_Seed_And_Commands_Produce_Same_Events()
|
||||||
{
|
{
|
||||||
|
|
@ -165,9 +211,20 @@ public class GameEngineTests
|
||||||
return engine;
|
return engine;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private GameEngine ReadyToAttackWithRearGuard()
|
||||||
|
{
|
||||||
|
var engine = StartedEngine();
|
||||||
|
Give(engine, 0, Marron);
|
||||||
|
engine.Execute(new RideCommand(Marron));
|
||||||
|
Give(engine, 0, Gallatin);
|
||||||
|
engine.Execute(new CallCommand(Gallatin, 0));
|
||||||
|
engine.Execute(new EnterBattleCommand());
|
||||||
|
return engine;
|
||||||
|
}
|
||||||
|
|
||||||
private List<string> BuildDeck()
|
private List<string> BuildDeck()
|
||||||
{
|
{
|
||||||
var mustInclude = new[] { Barcgal, Marron, Gallatin, Alfred };
|
var mustInclude = new[] { Barcgal, Marron, Gallatin, Alfred, Nike, Flogal, Govannon };
|
||||||
var names = mustInclude
|
var names = mustInclude
|
||||||
.Select(_catalog.CardOf)
|
.Select(_catalog.CardOf)
|
||||||
.Select(c => c.NameId)
|
.Select(c => c.NameId)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue