1423 lines
48 KiB
C#
1423 lines
48 KiB
C#
using Vanguard.Rules.Cards;
|
||
using Vanguard.Rules.Engine;
|
||
using Vanguard.Rules.Engine.Abilities;
|
||
using Xunit;
|
||
|
||
namespace Vanguard.Rules.Tests;
|
||
|
||
public class GameEngineTests
|
||
{
|
||
private readonly CardCatalog _catalog = TestCatalog.Instance;
|
||
|
||
private const string Barcgal = "BT01/003EN"; // grade 0, без триггера — стартовый Авангард
|
||
private const string Marron = "BT01/042EN"; // grade 1, 8000
|
||
private const string Gallatin = "BT01/021EN"; // grade 2, 10000
|
||
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
|
||
private const string Iseult = "BT01/011EN"; // Sentinel, Royal Paladin
|
||
private const string Nehalem = "BT01/022EN"; // Kagero
|
||
private const string Overlord = "BT01/004EN"; // Kagero, 11000
|
||
private const string Conroe = "BT01/016EN"; // Kagero, grade 0 без триггера
|
||
private const string BlasterBlade = "BT01/002EN";
|
||
private const string Wingal = "BT01/044EN";
|
||
private const string Lien = "BT01/043EN"; // activated: rest + discard → draw
|
||
private const string Cocoa = "BT01/007EN"; // OTT, смотрит верхнюю карту
|
||
private const string Libra = "BT01/017EN"; // OTT, grade 2
|
||
private const string Deathrex = "BT01/033EN"; // Tachikaze, 10000
|
||
private const string DemonEater = "BT01/036EN"; // Dark Irregulars, 10000
|
||
private const string Morgana = "TD01/011EN"; // Royal Paladin, grade 1, discard +4000
|
||
|
||
[Fact]
|
||
public void Start_Places_Starting_Vanguard_And_Draws_Five()
|
||
{
|
||
var engine = NewEngine();
|
||
engine.Start();
|
||
|
||
var player = engine.State.Players[0];
|
||
Assert.NotNull(player.Vanguard);
|
||
Assert.Equal(0, _catalog.CardOf(player.Vanguard!.PrintingId).Grade);
|
||
Assert.Equal(TriggerType.None, _catalog.CardOf(player.Vanguard.PrintingId).Trigger);
|
||
Assert.Equal(GameEngine.StartingHand, player.Hand.Count);
|
||
Assert.Equal(44, player.Deck.Count);
|
||
Assert.Equal(Phase.Mulligan, engine.State.Phase);
|
||
}
|
||
|
||
[Fact]
|
||
public void Ride_Rejects_Grade_Jump()
|
||
{
|
||
var engine = StartedEngine();
|
||
Give(engine, 0, Alfred);
|
||
|
||
var result = engine.Execute(new RideCommand(Alfred));
|
||
|
||
Assert.False(result.Accepted);
|
||
Assert.Equal(Conroe, engine.State.Players[0].Vanguard!.PrintingId);
|
||
}
|
||
|
||
[Fact]
|
||
public void Ride_Moves_Previous_Vanguard_To_Soul()
|
||
{
|
||
var engine = StartedEngine();
|
||
Give(engine, 0, Marron);
|
||
|
||
var result = engine.Execute(new RideCommand(Marron));
|
||
|
||
Assert.True(result.Accepted);
|
||
Assert.Equal(Marron, engine.State.Players[0].Vanguard!.PrintingId);
|
||
Assert.Contains(engine.State.Players[0].Soul, c => c.PrintingId == Conroe);
|
||
Assert.Equal(Phase.Main, engine.State.Phase);
|
||
}
|
||
|
||
[Fact]
|
||
public void Called_RearGuard_Enters_Rested_And_Slot_Is_Busy()
|
||
{
|
||
var engine = InMainPhase();
|
||
Give(engine, 0, Marron);
|
||
|
||
Assert.True(engine.Execute(new CallCommand(Marron, 0)).Accepted);
|
||
Assert.True(engine.State.Players[0].RearGuards[0]!.Rested);
|
||
|
||
Give(engine, 0, Marron);
|
||
var second = engine.Execute(new CallCommand(Marron, 0));
|
||
|
||
Assert.False(second.Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Attack_Without_Guard_Deals_Damage()
|
||
{
|
||
var engine = ReadyToAttack();
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
Assert.Single(engine.State.Players[1].Damage);
|
||
Assert.Equal(Step.None, engine.State.Step);
|
||
}
|
||
|
||
[Fact]
|
||
public void Guard_Prevents_Damage()
|
||
{
|
||
var engine = ReadyToAttack();
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
Give(engine, 1, Barcgal);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.True(engine.Execute(new GuardCommand(Barcgal)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
Assert.Empty(engine.State.Players[1].Damage);
|
||
Assert.Single(engine.State.Players[1].Drop);
|
||
}
|
||
|
||
[Fact]
|
||
public void Sixth_Damage_Ends_The_Game()
|
||
{
|
||
var engine = ReadyToAttack();
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
|
||
var defender = engine.State.Players[1];
|
||
for (var i = 0; i < GameEngine.MaxDamage - 1; i++)
|
||
defender.Damage.Add(TakeAnyFromDeck(defender));
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
Assert.True(engine.State.IsFinished);
|
||
Assert.Equal(0, engine.State.Winner);
|
||
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 Damage_Trigger_Also_Asks_For_Targets_And_Applies_Effect()
|
||
{
|
||
var engine = ReadyToAttack();
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
StackDeck(engine.State.Players[1], Nike);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
var pending = engine.State.PendingTrigger;
|
||
Assert.NotNull(pending);
|
||
Assert.Equal(TriggerContext.DamageCheck, pending!.Context);
|
||
|
||
// до разрешения карта лежит в зоне триггера, а не в уроне
|
||
Assert.Empty(engine.State.Players[1].Damage);
|
||
Assert.Contains(engine.State.Players[1].Trigger, c => c.PrintingId == Nike);
|
||
|
||
Assert.True(engine.Execute(new ResolveTriggerCommand(-1, -1)).Accepted);
|
||
|
||
Assert.Null(engine.State.PendingTrigger);
|
||
Assert.Null(engine.State.Battle);
|
||
Assert.Single(engine.State.Players[1].Damage);
|
||
Assert.Empty(engine.State.Players[1].Trigger);
|
||
Assert.Contains(engine.State.Players[1].Damage, c => c.PrintingId == Nike);
|
||
Assert.Contains(engine.Events, e => e is TriggerAppliedEvent { Player: 1 });
|
||
}
|
||
|
||
[Fact]
|
||
public void Trigger_Power_Lasts_Until_End_Of_Turn()
|
||
{
|
||
var engine = ReadyToAttack();
|
||
StackDeck(engine.State.Players[0], Nike, Gallatin);
|
||
|
||
engine.Execute(new AttackCommand(-1, -1));
|
||
engine.Execute(new PassGuardCommand());
|
||
engine.Execute(new ResolveTriggerCommand(-1, -1));
|
||
|
||
var vanguard = engine.State.Players[0].Vanguard!;
|
||
Assert.Equal(GameEngine.TriggerPowerBonus, vanguard.Modifiers.Sum(m => m.Power));
|
||
|
||
Assert.True(engine.Execute(new EndTurnCommand()).Accepted);
|
||
Assert.Empty(vanguard.Modifiers);
|
||
}
|
||
|
||
[Fact]
|
||
public void Boost_Adds_Back_Row_Power_To_Attacker()
|
||
{
|
||
var engine = ReadyToBoost();
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(3, -1, 0)).Accepted);
|
||
Assert.True(engine.State.Players[0].RearGuards[0]!.Rested);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
Assert.Single(engine.State.Players[1].Damage);
|
||
Assert.Contains(engine.Events, e => e is BoostedEvent { BoosterSlot: 0, AttackerSlot: 3 });
|
||
}
|
||
|
||
[Fact]
|
||
public void Without_Boost_The_Same_Attack_Misses()
|
||
{
|
||
var engine = ReadyToBoost();
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(3, -1)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
Assert.Empty(engine.State.Players[1].Damage);
|
||
Assert.False(engine.State.Players[0].RearGuards[0]!.Rested);
|
||
}
|
||
|
||
[Fact]
|
||
public void Boost_Rejects_Wrong_Column()
|
||
{
|
||
var engine = ReadyToBoost();
|
||
|
||
var result = engine.Execute(new AttackCommand(3, -1, 2));
|
||
|
||
Assert.False(result.Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Grade_Two_Cannot_Boost()
|
||
{
|
||
var engine = ReadyToBoost();
|
||
|
||
engine.State.Players[0].RearGuards[0] = TakeFromDeck(engine.State.Players[0], Gallatin);
|
||
|
||
var result = engine.Execute(new AttackCommand(3, -1, 0));
|
||
|
||
Assert.False(result.Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Effect_Can_Grant_Boost_To_Grade_Two()
|
||
{
|
||
var engine = ReadyToBoost();
|
||
var booster = TakeFromDeck(engine.State.Players[0], Gallatin);
|
||
engine.State.Players[0].RearGuards[0] = booster;
|
||
|
||
Assert.False(engine.Execute(new AttackCommand(3, -1, 0)).Accepted);
|
||
|
||
booster.BoostOverride = true;
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(3, -1, 0)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Effect_Can_Remove_Boost_From_Grade_One()
|
||
{
|
||
var engine = ReadyToBoost();
|
||
var booster = engine.State.Players[0].RearGuards[0]!;
|
||
|
||
booster.BoostOverride = false;
|
||
|
||
Assert.False(engine.Execute(new AttackCommand(3, -1, 0)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Perfect_Guard_Nullifies_Attack_When_Cost_Paid()
|
||
{
|
||
var engine = ReadyToAttack();
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
Give(engine, 1, Iseult);
|
||
Give(engine, 1, Marron);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.True(engine.Execute(new GuardCommand(Iseult, Marron)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
Assert.Empty(engine.State.Players[1].Damage);
|
||
Assert.Contains(engine.State.Players[1].Drop, c => c.PrintingId == Marron);
|
||
Assert.Contains(engine.Events, e => e is PerfectGuardEvent);
|
||
}
|
||
|
||
[Fact]
|
||
public void Perfect_Guard_Without_Cost_Adds_No_Shield()
|
||
{
|
||
var engine = ReadyToAttack();
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
Give(engine, 1, Iseult);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.True(engine.Execute(new GuardCommand(Iseult)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
Assert.Single(engine.State.Players[1].Damage);
|
||
Assert.DoesNotContain(engine.Events, e => e is PerfectGuardEvent);
|
||
}
|
||
|
||
[Fact]
|
||
public void Perfect_Guard_Rejects_Wrong_Clan_Cost()
|
||
{
|
||
var engine = ReadyToAttack();
|
||
Give(engine, 1, Iseult);
|
||
Give(engine, 1, Nehalem);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.False(engine.Execute(new GuardCommand(Iseult, Nehalem)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Intercept_Saves_Target_And_Retires_Interceptor()
|
||
{
|
||
var engine = ReadyToAttackRearGuard();
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(3, 0)).Accepted);
|
||
Assert.True(engine.Execute(new InterceptCommand(3)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
var defender = engine.State.Players[1];
|
||
Assert.NotNull(defender.RearGuards[0]); // цель выжила
|
||
Assert.Null(defender.RearGuards[3]); // перехватчик покинул круг
|
||
Assert.Contains(defender.Drop, c => c.PrintingId == Gallatin);
|
||
Assert.Contains(engine.Events, e => e is InterceptedEvent);
|
||
Assert.Contains(engine.Events, e => e is BattleResolvedEvent { Hit: false });
|
||
}
|
||
|
||
[Fact]
|
||
public void Without_Intercept_RearGuard_Is_Retired()
|
||
{
|
||
var engine = ReadyToAttackRearGuard();
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(3, 0)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
var defender = engine.State.Players[1];
|
||
Assert.Null(defender.RearGuards[0]);
|
||
Assert.Contains(defender.Drop, c => c.PrintingId == Marron);
|
||
Assert.Contains(engine.Events, e => e is UnitRetiredEvent);
|
||
}
|
||
|
||
[Fact]
|
||
public void Grade_One_Cannot_Intercept()
|
||
{
|
||
var engine = ReadyToAttackRearGuard();
|
||
var defender = engine.State.Players[1];
|
||
defender.RearGuards[3] = TakeFromDeck(defender, Marron);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(3, 0)).Accepted);
|
||
Assert.False(engine.Execute(new InterceptCommand(3)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Back_Row_Cannot_Intercept_By_Default()
|
||
{
|
||
var engine = ReadyToAttackRearGuard();
|
||
var defender = engine.State.Players[1];
|
||
defender.RearGuards[1] = TakeFromDeck(defender, Gallatin);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(3, 0)).Accepted);
|
||
Assert.False(engine.Execute(new InterceptCommand(1)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Effect_Can_Grant_Back_Row_Intercept()
|
||
{
|
||
var engine = ReadyToAttackRearGuard();
|
||
var defender = engine.State.Players[1];
|
||
var unit = TakeFromDeck(defender, Gallatin);
|
||
unit.InterceptOverride = true;
|
||
unit.InterceptFromBackRow = true;
|
||
defender.RearGuards[1] = unit;
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(3, 0)).Accepted);
|
||
Assert.True(engine.Execute(new InterceptCommand(1)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Intercept_Works_Against_Vanguard_Attack()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
SetVanguard(engine, 0, Overlord);
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
engine.Execute(new EnterBattleCommand());
|
||
|
||
var defender = engine.State.Players[1];
|
||
defender.RearGuards[3] = TakeFromDeck(defender, Gallatin);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.True(engine.Execute(new InterceptCommand(3)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
Assert.Empty(defender.Damage);
|
||
Assert.NotNull(defender.Vanguard);
|
||
Assert.Contains(defender.Drop, c => c.PrintingId == Gallatin);
|
||
Assert.Contains(engine.Events, e => e is InterceptorRetiredEvent);
|
||
}
|
||
|
||
[Fact]
|
||
public void Attacked_RearGuard_Cannot_Intercept_Itself()
|
||
{
|
||
var engine = ReadyToAttackRearGuard();
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(3, 0)).Accepted);
|
||
Assert.False(engine.Execute(new InterceptCommand(0)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Move_To_Empty_Circle_In_Same_Column()
|
||
{
|
||
var engine = InMainPhase();
|
||
Give(engine, 0, Marron);
|
||
engine.Execute(new CallCommand(Marron, 0));
|
||
|
||
Assert.True(engine.Execute(new MoveCommand(0, 3)).Accepted);
|
||
|
||
Assert.Null(engine.State.Players[0].RearGuards[0]);
|
||
Assert.NotNull(engine.State.Players[0].RearGuards[3]);
|
||
Assert.Contains(engine.Events, e => e is UnitMovedEvent { FromSlot: 0, ToSlot: 3 });
|
||
}
|
||
|
||
[Fact]
|
||
public void Move_Swaps_Occupied_Circles()
|
||
{
|
||
var engine = InMainPhase();
|
||
Give(engine, 0, Marron);
|
||
engine.Execute(new CallCommand(Marron, 0));
|
||
Give(engine, 0, Gallatin);
|
||
engine.Execute(new CallCommand(Gallatin, 3));
|
||
|
||
Assert.True(engine.Execute(new MoveCommand(0, 3)).Accepted);
|
||
|
||
Assert.Equal(Marron, engine.State.Players[0].RearGuards[3]!.PrintingId);
|
||
Assert.Equal(Gallatin, engine.State.Players[0].RearGuards[0]!.PrintingId);
|
||
}
|
||
|
||
[Fact]
|
||
public void Move_Rejects_Other_Column()
|
||
{
|
||
var engine = InMainPhase();
|
||
Give(engine, 0, Marron);
|
||
engine.Execute(new CallCommand(Marron, 0));
|
||
|
||
Assert.False(engine.Execute(new MoveCommand(0, 4)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Center_Back_Row_Has_No_Pair()
|
||
{
|
||
var engine = InMainPhase();
|
||
Give(engine, 0, Marron);
|
||
engine.Execute(new CallCommand(Marron, 1));
|
||
|
||
Assert.False(engine.Execute(new MoveCommand(1, 3)).Accepted);
|
||
Assert.False(engine.Execute(new MoveCommand(1, 0)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Move_Is_Available_Only_In_Main_Phase()
|
||
{
|
||
var engine = StartedEngine(); // фаза Смены
|
||
|
||
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<DriveCheckedEvent>());
|
||
}
|
||
|
||
[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<DriveCheckedEvent>().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<DriveCheckedEvent>());
|
||
}
|
||
|
||
[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<DriveCheckedEvent>());
|
||
}
|
||
|
||
[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<DriveCheckedEvent>());
|
||
}
|
||
|
||
[Fact]
|
||
public void Barcgal_Offers_Optional_Call_When_Ridden_Over()
|
||
{
|
||
var engine = StartedEngine();
|
||
SetVanguard(engine, 0, Barcgal);
|
||
Give(engine, 0, Marron);
|
||
|
||
Assert.True(engine.Execute(new RideCommand(Marron)).Accepted);
|
||
Assert.NotNull(engine.State.PendingAbility);
|
||
Assert.Equal(Step.Ability, engine.State.Step);
|
||
|
||
Assert.True(engine.Execute(new ResolveAbilityCommand(true)).Accepted);
|
||
|
||
Assert.Null(engine.State.PendingAbility);
|
||
Assert.Contains(engine.State.Players[0].RearGuards, u => u?.PrintingId == Barcgal);
|
||
Assert.DoesNotContain(engine.State.Players[0].Soul, c => c.PrintingId == Barcgal);
|
||
}
|
||
|
||
[Fact]
|
||
public void Barcgal_Decline_Keeps_Card_In_Soul()
|
||
{
|
||
var engine = StartedEngine();
|
||
SetVanguard(engine, 0, Barcgal);
|
||
Give(engine, 0, Marron);
|
||
|
||
engine.Execute(new RideCommand(Marron));
|
||
Assert.True(engine.Execute(new ResolveAbilityCommand(false)).Accepted);
|
||
|
||
Assert.Contains(engine.State.Players[0].Soul, c => c.PrintingId == Barcgal);
|
||
Assert.DoesNotContain(engine.State.Players[0].RearGuards, u => u?.PrintingId == Barcgal);
|
||
}
|
||
|
||
[Fact]
|
||
public void Wingal_Gives_BlasterBlade_4000_During_Battle()
|
||
{
|
||
var engine = ReadyToAttackWithWingal(BlasterBlade);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(3, -1, 0)).Accepted);
|
||
Assert.Equal(4000, engine.State.Players[0].RearGuards[3]!.Modifiers.Where(m => m.EffectId == "ability").Sum(m => m.Power));
|
||
}
|
||
|
||
[Fact]
|
||
public void Wingal_Does_Not_Boost_Other_Units()
|
||
{
|
||
var engine = ReadyToAttackWithWingal(Gallatin);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(3, -1, 0)).Accepted);
|
||
Assert.Equal(0, engine.State.Players[0].RearGuards[3]!.Modifiers.Where(m => m.EffectId == "ability").Sum(m => m.Power));
|
||
}
|
||
|
||
[Fact]
|
||
public void Alfred_Gets_2000_Per_RoyalPaladin_RearGuard()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
SetVanguard(engine, 0, Alfred);
|
||
|
||
Give(engine, 0, Marron);
|
||
engine.Execute(new CallCommand(Marron, 0));
|
||
Give(engine, 0, Marron);
|
||
engine.Execute(new CallCommand(Marron, 3));
|
||
|
||
var alfred = engine.State.Players[0].Vanguard!;
|
||
Assert.Equal(14000, engine.EffectivePower(alfred));
|
||
}
|
||
|
||
[Fact]
|
||
public void Guard_Protects_RearGuard_From_Retirement()
|
||
{
|
||
var engine = ReadyToAttackRearGuard();
|
||
Give(engine, 1, Barcgal);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(3, 0)).Accepted);
|
||
Assert.True(engine.Execute(new GuardCommand(Barcgal)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
var defender = engine.State.Players[1];
|
||
Assert.NotNull(defender.RearGuards[0]);
|
||
Assert.Contains(defender.Drop, c => c.PrintingId == Barcgal);
|
||
Assert.DoesNotContain(engine.Events, e => e is UnitRetiredEvent);
|
||
}
|
||
|
||
[Fact]
|
||
public void BlasterBlade_On_Vanguard_Retires_Chosen_RearGuard()
|
||
{
|
||
var engine = StartedEngine();
|
||
SetVanguard(engine, 0, Marron);
|
||
GiveDamage(engine, 0, 2);
|
||
|
||
var opponent = engine.State.Players[1];
|
||
opponent.RearGuards[0] = TakeFromDeck(opponent, Gallatin);
|
||
|
||
Give(engine, 0, BlasterBlade);
|
||
Assert.True(engine.Execute(new RideCommand(BlasterBlade)).Accepted);
|
||
|
||
Assert.NotNull(engine.State.PendingAbility);
|
||
Assert.True(engine.Execute(new ResolveAbilityCommand(true)).Accepted);
|
||
|
||
Assert.NotNull(engine.State.PendingChoice);
|
||
Assert.Single(engine.State.PendingChoice!.Options);
|
||
|
||
Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
|
||
|
||
Assert.Null(opponent.RearGuards[0]);
|
||
Assert.Equal(2, engine.State.Players[0].Damage.Count(c => c.FaceUp));
|
||
}
|
||
|
||
[Fact]
|
||
public void BlasterBlade_Is_Not_Offered_Without_Counter_Blast()
|
||
{
|
||
var engine = StartedEngine();
|
||
SetVanguard(engine, 0, Marron);
|
||
Give(engine, 0, BlasterBlade);
|
||
|
||
Assert.True(engine.Execute(new RideCommand(BlasterBlade)).Accepted);
|
||
|
||
Assert.Null(engine.State.PendingAbility);
|
||
}
|
||
|
||
[Fact]
|
||
public void BlasterBlade_On_RearGuard_Requires_RoyalPaladin_Vanguard()
|
||
{
|
||
var kagero = StartedEngine();
|
||
kagero.Execute(new SkipRideCommand());
|
||
Give(kagero, 0, BlasterBlade);
|
||
kagero.Execute(new CallCommand(BlasterBlade, 0));
|
||
|
||
Assert.Null(kagero.State.PendingAbility);
|
||
|
||
var royal = StartedEngine();
|
||
SetVanguard(royal, 0, Marron);
|
||
royal.Execute(new SkipRideCommand());
|
||
GiveDamage(royal, 0, 2);
|
||
Give(royal, 0, BlasterBlade);
|
||
royal.Execute(new CallCommand(BlasterBlade, 0));
|
||
|
||
Assert.NotNull(royal.State.PendingAbility);
|
||
}
|
||
|
||
[Fact]
|
||
public void Activated_Ability_Rests_Self_Discards_And_Draws()
|
||
{
|
||
var engine = InMainPhase();
|
||
Give(engine, 0, Lien);
|
||
engine.Execute(new CallCommand(Lien, 0));
|
||
|
||
var lien = engine.State.Players[0].RearGuards[0]!;
|
||
lien.Rested = false;
|
||
|
||
Give(engine, 0, Marron);
|
||
var handBefore = engine.State.Players[0].Hand.Count;
|
||
|
||
Assert.True(engine.Execute(new ActivateAbilityCommand(lien.Id, Marron)).Accepted);
|
||
|
||
Assert.True(lien.Rested);
|
||
Assert.Contains(engine.State.Players[0].Drop, c => c.PrintingId == Marron);
|
||
Assert.Equal(handBefore, engine.State.Players[0].Hand.Count); // -1 сброс, +1 добор
|
||
}
|
||
|
||
[Fact]
|
||
public void Activated_Ability_Requires_Discard()
|
||
{
|
||
var engine = InMainPhase();
|
||
Give(engine, 0, Lien);
|
||
engine.Execute(new CallCommand(Lien, 0));
|
||
var lien = engine.State.Players[0].RearGuards[0]!;
|
||
lien.Rested = false;
|
||
|
||
Assert.False(engine.Execute(new ActivateAbilityCommand(lien.Id)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Activated_Ability_Rejects_Rested_Card()
|
||
{
|
||
var engine = InMainPhase();
|
||
Give(engine, 0, Lien);
|
||
engine.Execute(new CallCommand(Lien, 0));
|
||
var lien = engine.State.Players[0].RearGuards[0]!;
|
||
Give(engine, 0, Marron);
|
||
|
||
Assert.False(engine.Execute(new ActivateAbilityCommand(lien.Id, Marron)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Search_Without_Matches_Only_Shuffles()
|
||
{
|
||
var engine = StartedEngine();
|
||
var handBefore = engine.State.Players[0].Hand.Count;
|
||
|
||
engine.SearchDeckToHand(0, "Карта X", d => d.NameId == "no-such-card");
|
||
|
||
Assert.Contains(engine.Events, e => e is SearchFailedEvent);
|
||
Assert.Null(engine.State.PendingChoice);
|
||
Assert.Equal(handBefore, engine.State.Players[0].Hand.Count);
|
||
}
|
||
|
||
[Fact]
|
||
public void Search_With_Single_Match_Puts_Card_Into_Hand()
|
||
{
|
||
var engine = StartedEngine();
|
||
KeepOnly(engine, 0, "wingal", 1);
|
||
|
||
engine.SearchDeckToHand(0, "Wingal", d => d.NameId == "wingal");
|
||
|
||
Assert.Null(engine.State.PendingChoice);
|
||
Assert.Contains(engine.State.Players[0].Hand, c => c.PrintingId == Wingal);
|
||
Assert.DoesNotContain(engine.State.Players[0].Deck, c => c.PrintingId == Wingal);
|
||
}
|
||
|
||
[Fact]
|
||
public void Search_With_Two_Arts_Offers_Two_Options()
|
||
{
|
||
var engine = StartedEngine();
|
||
KeepOnly(engine, 0, "blaster-blade", 1);
|
||
|
||
var parallel = new CardInstance { Id = 9001, PrintingId = "BT01/S02EN" };
|
||
engine.State.Players[0].Deck.Add(parallel);
|
||
|
||
engine.SearchDeckToHand(0, "Blaster Blade", d => d.NameId == "blaster-blade");
|
||
|
||
var choice = engine.State.PendingChoice;
|
||
Assert.NotNull(choice);
|
||
Assert.Equal(2, choice!.Options.Count);
|
||
|
||
Assert.True(engine.Execute(new ChooseCommand(1)).Accepted);
|
||
Assert.Contains(engine.State.Players[0].Hand, c => c.PrintingId == "BT01/S02EN");
|
||
}
|
||
|
||
[Fact]
|
||
public void Conroe_Retires_Itself_Pays_Counter_Blast_And_Searches()
|
||
{
|
||
var engine = InMainPhase();
|
||
Give(engine, 0, Conroe);
|
||
engine.Execute(new CallCommand(Conroe, 0));
|
||
|
||
var conroe = engine.State.Players[0].RearGuards[0]!;
|
||
conroe.Rested = false;
|
||
GiveDamage(engine, 0, 1);
|
||
|
||
Assert.True(engine.Execute(new ActivateAbilityCommand(conroe.Id)).Accepted);
|
||
|
||
Assert.Null(engine.State.Players[0].RearGuards[0]);
|
||
Assert.Contains(engine.State.Players[0].Drop, c => c.PrintingId == Conroe);
|
||
Assert.Equal(1, engine.State.Players[0].Damage.Count(c => c.FaceUp));
|
||
|
||
if (engine.State.PendingChoice is not null)
|
||
Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
|
||
|
||
var added = engine.State.Players[0].Hand[^1];
|
||
var def = _catalog.CardOf(added.PrintingId);
|
||
Assert.Equal(Clan.Kagero, def.Clan);
|
||
Assert.True(def.Grade <= 1);
|
||
}
|
||
|
||
[Fact]
|
||
public void Cocoa_Can_Keep_The_Top_Card_On_Top()
|
||
{
|
||
var engine = InMainPhase();
|
||
SetVanguard(engine, 0, Libra);
|
||
Give(engine, 0, Cocoa);
|
||
engine.Execute(new CallCommand(Cocoa, 0));
|
||
|
||
var choice = engine.State.PendingChoice;
|
||
Assert.NotNull(choice);
|
||
Assert.Equal(2, choice!.Options.Count);
|
||
|
||
var topBefore = engine.State.Players[0].Deck[0].PrintingId;
|
||
Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
|
||
|
||
Assert.Equal(topBefore, engine.State.Players[0].Deck[0].PrintingId);
|
||
}
|
||
|
||
[Fact]
|
||
public void Cocoa_Can_Put_The_Top_Card_On_Bottom()
|
||
{
|
||
var engine = InMainPhase();
|
||
SetVanguard(engine, 0, Libra);
|
||
Give(engine, 0, Cocoa);
|
||
engine.Execute(new CallCommand(Cocoa, 0));
|
||
|
||
var topBefore = engine.State.Players[0].Deck[0].PrintingId;
|
||
Assert.True(engine.Execute(new ChooseCommand(1)).Accepted);
|
||
|
||
Assert.Equal(topBefore, engine.State.Players[0].Deck[^1].PrintingId);
|
||
}
|
||
|
||
[Fact]
|
||
public void Alfred_Searches_And_Calls_A_RoyalPaladin()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
SetVanguard(engine, 0, Alfred);
|
||
GiveDamage(engine, 0, 3);
|
||
|
||
var alfred = engine.State.Players[0].Vanguard!;
|
||
Assert.True(engine.Execute(new ActivateAbilityCommand(alfred.Id)).Accepted);
|
||
|
||
Assert.Equal(3, engine.State.Players[0].Damage.Count(c => c.FaceUp));
|
||
|
||
if (engine.State.PendingChoice is not null)
|
||
Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
|
||
|
||
if (engine.State.PendingChoice is not null)
|
||
Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
|
||
|
||
var called = engine.State.Players[0].RearGuards.FirstOrDefault(u => u is not null);
|
||
Assert.NotNull(called);
|
||
|
||
var def = _catalog.CardOf(called!.PrintingId);
|
||
Assert.Equal(Clan.RoyalPaladin, def.Clan);
|
||
Assert.True(def.Grade <= 2);
|
||
}
|
||
|
||
[Fact]
|
||
public void Search_Can_Ride_The_Found_Card()
|
||
{
|
||
var engine = InMainPhase();
|
||
|
||
engine.SearchDeckToRide(0, "Blaster Blade", d => d.NameId == "blaster-blade");
|
||
|
||
if (engine.State.PendingChoice is not null)
|
||
Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
|
||
|
||
var vanguard = engine.State.Players[0].Vanguard!;
|
||
Assert.Equal("blaster-blade", _catalog.CardOf(vanguard.PrintingId).NameId);
|
||
}
|
||
|
||
[Fact]
|
||
public void Deathrex_Gains_5000_When_Attacking()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
SetVanguard(engine, 0, Deathrex);
|
||
engine.Execute(new EnterBattleCommand());
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
|
||
Assert.Equal(5000, engine.State.Players[0].Vanguard!.Modifiers.Sum(m => m.Power));
|
||
}
|
||
|
||
[Fact]
|
||
public void DemonEater_Charges_Soul_At_Main_Phase_Start()
|
||
{
|
||
var engine = StartedEngine();
|
||
SetVanguard(engine, 0, DemonEater);
|
||
engine.Execute(new SkipRideCommand());
|
||
|
||
var vanguard = engine.State.Players[0].Vanguard!;
|
||
Assert.Single(engine.State.Players[0].Soul);
|
||
Assert.Equal(12000, engine.EffectivePower(vanguard));
|
||
}
|
||
|
||
[Fact]
|
||
public void MaidenOfLibra_Draws_On_Hit_When_Cost_Paid()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
SetVanguard(engine, 0, Libra);
|
||
GiveDamage(engine, 0, 2);
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
engine.Execute(new EnterBattleCommand());
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
Assert.NotNull(engine.State.PendingAbility);
|
||
var handBefore = engine.State.Players[0].Hand.Count;
|
||
|
||
Assert.True(engine.Execute(new ResolveAbilityCommand(true)).Accepted);
|
||
|
||
Assert.Equal(2, engine.State.Players[0].Damage.Count(c => c.FaceUp));
|
||
Assert.Equal(handBefore + 1, engine.State.Players[0].Hand.Count);
|
||
Assert.Single(engine.State.Players[1].Damage);
|
||
}
|
||
|
||
[Fact]
|
||
public void Attack_Hit_Effects_Run_After_Damage()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
SetVanguard(engine, 0, Libra);
|
||
GiveDamage(engine, 0, 2);
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
engine.Execute(new EnterBattleCommand());
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
// урон уже нанесён, и только теперь спрашивают про эффект попадания
|
||
Assert.Single(engine.State.Players[1].Damage);
|
||
Assert.NotNull(engine.State.PendingAbility);
|
||
}
|
||
|
||
[Fact]
|
||
public void Morgana_Discards_To_Gain_4000_When_Attacking()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
SetVanguard(engine, 0, Morgana);
|
||
engine.Execute(new EnterBattleCommand());
|
||
|
||
var morgana = engine.State.Players[0].Vanguard!;
|
||
var handBefore = engine.State.Players[0].Hand.Count;
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.NotNull(engine.State.PendingAbility);
|
||
|
||
Assert.True(engine.Execute(new ResolveAbilityCommand(true)).Accepted);
|
||
Assert.NotNull(engine.State.PendingChoice);
|
||
|
||
Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
|
||
|
||
Assert.Equal(4000, morgana.Modifiers.Sum(m => m.Power));
|
||
Assert.Equal(handBefore - 1, engine.State.Players[0].Hand.Count);
|
||
Assert.Equal(Step.Guard, engine.State.Step);
|
||
}
|
||
|
||
[Fact]
|
||
public void Battle_Modifiers_Expire_But_Turn_Modifiers_Stay()
|
||
{
|
||
var engine = StartedEngine();
|
||
SetVanguard(engine, 0, DemonEater);
|
||
engine.Execute(new SkipRideCommand());
|
||
|
||
var vanguard = engine.State.Players[0].Vanguard!;
|
||
Assert.Equal(2000, vanguard.Modifiers.Sum(m => m.Power));
|
||
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
engine.Execute(new EnterBattleCommand());
|
||
engine.Execute(new AttackCommand(-1, -1));
|
||
engine.Execute(new PassGuardCommand());
|
||
|
||
Assert.Equal(2000, vanguard.Modifiers.Sum(m => m.Power));
|
||
|
||
engine.Execute(new EndTurnCommand());
|
||
Assert.Empty(vanguard.Modifiers);
|
||
}
|
||
|
||
[Fact]
|
||
public void Granted_Ability_Triggers()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
SetVanguard(engine, 0, Gallatin);
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
engine.Execute(new EnterBattleCommand());
|
||
|
||
var vanguard = engine.State.Players[0].Vanguard!;
|
||
engine.GrantAbility(
|
||
vanguard,
|
||
new AbilityDefinition
|
||
{
|
||
Trigger = AbilityTrigger.OnAttackDeclared,
|
||
Resolve = ctx => ctx.Engine.AddBattlePower(ctx.Source, 3000),
|
||
},
|
||
vanguard,
|
||
"test",
|
||
ModifierDuration.UntilEndOfTurn);
|
||
|
||
engine.Execute(new AttackCommand(-1, -1));
|
||
|
||
Assert.Equal(3000, vanguard.Modifiers.Where(m => m.EffectId == "ability").Sum(m => m.Power));
|
||
}
|
||
|
||
[Fact]
|
||
public void Granted_Ability_Can_Be_Removed()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
SetVanguard(engine, 0, Gallatin);
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
engine.Execute(new EnterBattleCommand());
|
||
|
||
var vanguard = engine.State.Players[0].Vanguard!;
|
||
engine.GrantAbility(
|
||
vanguard,
|
||
new AbilityDefinition
|
||
{
|
||
Trigger = AbilityTrigger.OnAttackDeclared,
|
||
Resolve = ctx => ctx.Engine.AddBattlePower(ctx.Source, 3000),
|
||
},
|
||
vanguard,
|
||
"test",
|
||
ModifierDuration.UntilEndOfTurn);
|
||
|
||
engine.RemoveGrantedAbilities(g => g.EffectId == "test");
|
||
engine.Execute(new AttackCommand(-1, -1));
|
||
|
||
Assert.Equal(0, vanguard.Modifiers.Where(m => m.EffectId == "ability").Sum(m => m.Power));
|
||
}
|
||
|
||
[Fact]
|
||
public void MultiChoice_Stops_When_Player_Says_Done()
|
||
{
|
||
var engine = StartedEngine();
|
||
var opponent = engine.State.Players[1];
|
||
opponent.RearGuards[0] = TakeFromDeck(opponent, Marron);
|
||
opponent.RearGuards[1] = TakeFromDeck(opponent, Gallatin);
|
||
opponent.RearGuards[3] = TakeFromDeck(opponent, Gallatin);
|
||
|
||
IReadOnlyList<UnitTarget>? result = null;
|
||
engine.AskUnitMultiChoice(
|
||
0,
|
||
"Выберите до двух",
|
||
new[] { new UnitTarget(1, 0), new UnitTarget(1, 1), new UnitTarget(1, 3) },
|
||
0,
|
||
2,
|
||
chosen => result = chosen);
|
||
|
||
Assert.Equal(Step.Choice, engine.State.Step);
|
||
Assert.Equal(4, engine.State.PendingChoice!.Options.Count); // три цели и «Готово»
|
||
|
||
engine.Execute(new ChooseCommand(0));
|
||
engine.Execute(new ChooseCommand(2)); // «Готово» среди двух оставшихся
|
||
|
||
Assert.NotNull(result);
|
||
Assert.Single(result!);
|
||
Assert.Equal(0, result![0].Slot);
|
||
Assert.Equal(Step.None, engine.State.Step);
|
||
}
|
||
|
||
[Fact]
|
||
public void MultiChoice_Resolves_Automatically_When_Max_Reached()
|
||
{
|
||
var engine = StartedEngine();
|
||
var opponent = engine.State.Players[1];
|
||
opponent.RearGuards[0] = TakeFromDeck(opponent, Marron);
|
||
opponent.RearGuards[1] = TakeFromDeck(opponent, Gallatin);
|
||
|
||
IReadOnlyList<UnitTarget>? result = null;
|
||
engine.AskUnitMultiChoice(
|
||
0,
|
||
"Выберите ровно две",
|
||
new[] { new UnitTarget(1, 0), new UnitTarget(1, 1) },
|
||
2,
|
||
2,
|
||
chosen => result = chosen);
|
||
|
||
Assert.Equal(2, engine.State.PendingChoice!.Options.Count); // две цели, «Готово» ещё нет
|
||
|
||
engine.Execute(new ChooseCommand(0));
|
||
Assert.Null(result); // после первой цели выбор ещё продолжается
|
||
engine.Execute(new ChooseCommand(0));
|
||
|
||
Assert.NotNull(result);
|
||
Assert.Equal(2, result!.Count);
|
||
}
|
||
|
||
[Fact]
|
||
public void CannotStand_Skips_Stand_And_Expires_After_Stand_Phase()
|
||
{
|
||
var engine = StartedEngine();
|
||
SetVanguard(engine, 0, Gallatin);
|
||
engine.Execute(new SkipRideCommand());
|
||
|
||
var vanguard = engine.State.Players[0].Vanguard!;
|
||
engine.AddRestriction(
|
||
vanguard,
|
||
RestrictionType.CannotStand,
|
||
vanguard,
|
||
"test",
|
||
ModifierDuration.UntilOwnerNextStand);
|
||
vanguard.Rested = true;
|
||
|
||
engine.Execute(new EndTurnCommand()); // ход переходит к игроку 1
|
||
engine.Execute(new SkipRideCommand()); // игрок 1 выходит в Основную фазу
|
||
engine.Execute(new EndTurnCommand()); // и обратно
|
||
|
||
Assert.True(vanguard.Rested);
|
||
Assert.Empty(vanguard.Restrictions);
|
||
}
|
||
|
||
[Fact]
|
||
public void CannotAttack_Restriction_Blocks_Attack()
|
||
{
|
||
var engine = ReadyToAttackRearGuard();
|
||
var attacker = engine.State.Players[0].RearGuards[3]!;
|
||
engine.AddRestriction(
|
||
attacker,
|
||
RestrictionType.CannotAttack,
|
||
attacker,
|
||
"test",
|
||
ModifierDuration.UntilEndOfTurn);
|
||
|
||
Assert.False(engine.Execute(new AttackCommand(3, 0)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Attack_Hit_On_RearGuard_Triggers_OnAttackHit()
|
||
{
|
||
var engine = ReadyToAttackRearGuard();
|
||
var attacker = engine.State.Players[0].RearGuards[3]!;
|
||
|
||
engine.GrantAbility(
|
||
attacker,
|
||
new AbilityDefinition
|
||
{
|
||
Trigger = AbilityTrigger.OnAttackHit,
|
||
Resolve = ctx => ctx.Engine.AddPower(
|
||
ctx.Source,
|
||
2000,
|
||
ctx.Source,
|
||
"hit",
|
||
ModifierDuration.UntilEndOfTurn),
|
||
},
|
||
attacker,
|
||
"test",
|
||
ModifierDuration.UntilEndOfTurn);
|
||
|
||
engine.Execute(new AttackCommand(3, 0));
|
||
engine.Execute(new PassGuardCommand());
|
||
|
||
Assert.Null(engine.State.Players[1].RearGuards[0]);
|
||
Assert.Equal(2000, attacker.Modifiers.Where(m => m.EffectId == "hit").Sum(m => m.Power));
|
||
}
|
||
|
||
[Fact]
|
||
public void ResolveTrigger_Rejects_When_Nothing_Is_Pending()
|
||
{
|
||
var engine = StartedEngine();
|
||
|
||
Assert.False(engine.Execute(new ResolveTriggerCommand(-1, -1)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Same_Seed_And_Commands_Produce_Same_Events()
|
||
{
|
||
var first = RunScriptedMatch(seed: 12345);
|
||
var second = RunScriptedMatch(seed: 12345);
|
||
|
||
Assert.Equal(first, second);
|
||
}
|
||
|
||
private List<GameEvent> RunScriptedMatch(ulong seed)
|
||
{
|
||
var engine = NewEngine(seed);
|
||
engine.Start();
|
||
engine.Execute(new FinishMulliganCommand());
|
||
Give(engine, 0, Marron);
|
||
engine.Execute(new RideCommand(Marron));
|
||
engine.Execute(new AttackCommand(-1, -1));
|
||
engine.Execute(new PassGuardCommand());
|
||
return engine.Events.ToList();
|
||
}
|
||
|
||
// ---------- helpers ----------
|
||
|
||
private GameEngine NewEngine(ulong seed = 7) =>
|
||
new(_catalog, seed, BuildDeck(), BuildDeck());
|
||
|
||
private GameEngine StartedEngine()
|
||
{
|
||
var engine = NewEngine();
|
||
engine.Start();
|
||
engine.Execute(new FinishMulliganCommand());
|
||
|
||
// фиксируем стартовый Авангард, чтобы Barcgal не срабатывал в обычных тестах
|
||
SetVanguard(engine, 0, Conroe);
|
||
return engine;
|
||
}
|
||
|
||
private GameEngine InMainPhase()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
return engine;
|
||
}
|
||
|
||
private GameEngine ReadyToAttack()
|
||
{
|
||
var engine = StartedEngine();
|
||
Give(engine, 0, Marron);
|
||
engine.Execute(new RideCommand(Marron));
|
||
engine.Execute(new EnterBattleCommand());
|
||
return engine;
|
||
}
|
||
|
||
private GameEngine ReadyToAttackWithWingal(string attackerPrintingId)
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
|
||
Give(engine, 0, attackerPrintingId);
|
||
engine.Execute(new CallCommand(attackerPrintingId, 3));
|
||
|
||
Give(engine, 0, Wingal);
|
||
engine.Execute(new CallCommand(Wingal, 0));
|
||
|
||
engine.State.Players[0].RearGuards[0]!.Rested = false;
|
||
engine.State.Players[0].RearGuards[3]!.Rested = false;
|
||
|
||
engine.Execute(new EnterBattleCommand());
|
||
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 GameEngine ReadyToBoost()
|
||
{
|
||
var engine = StartedEngine();
|
||
Give(engine, 0, Marron);
|
||
engine.Execute(new RideCommand(Marron));
|
||
|
||
// сзади слева — поддержка (1 ранг), впереди слева — атакующий (2 ранг)
|
||
Give(engine, 0, Marron);
|
||
engine.Execute(new CallCommand(Marron, 0));
|
||
|
||
Give(engine, 0, Gallatin);
|
||
engine.Execute(new CallCommand(Gallatin, 3));
|
||
|
||
// призванные карты входят повёрнутыми — для теста поддержки поднимаем обе
|
||
engine.State.Players[0].RearGuards[0]!.Rested = false;
|
||
engine.State.Players[0].RearGuards[3]!.Rested = false;
|
||
|
||
SetVanguard(engine, 1, Gallatin);
|
||
engine.Execute(new EnterBattleCommand());
|
||
return engine;
|
||
}
|
||
|
||
private GameEngine ReadyToAttackRearGuard()
|
||
{
|
||
var engine = StartedEngine();
|
||
Give(engine, 0, Marron);
|
||
engine.Execute(new RideCommand(Marron));
|
||
|
||
Give(engine, 0, Gallatin);
|
||
engine.Execute(new CallCommand(Gallatin, 3));
|
||
engine.State.Players[0].RearGuards[3]!.Rested = false;
|
||
|
||
var defender = engine.State.Players[1];
|
||
defender.RearGuards[0] = TakeFromDeck(defender, Marron);
|
||
defender.RearGuards[3] = TakeFromDeck(defender, Gallatin);
|
||
|
||
engine.Execute(new EnterBattleCommand());
|
||
return engine;
|
||
}
|
||
|
||
private static readonly (string PrintingId, int Count)[] Seeds =
|
||
{
|
||
(Barcgal, 4), (Marron, 4), (Gallatin, 4), (Alfred, 4),
|
||
(Nike, 2), (Flogal, 2), (Govannon, 2),
|
||
(Iseult, 2), (Nehalem, 2), (Overlord, 2), (Conroe, 4),
|
||
(BlasterBlade, 2), (Wingal, 2),
|
||
(Lien, 2),
|
||
(Cocoa, 2), (Libra, 2),
|
||
(Deathrex, 2), (DemonEater, 2),
|
||
(Morgana, 2),
|
||
};
|
||
|
||
private List<string> BuildDeck()
|
||
{
|
||
var names = Seeds
|
||
.Select(s => _catalog.CardOf(s.PrintingId).NameId)
|
||
.ToHashSet(StringComparer.Ordinal);
|
||
|
||
var deck = new List<string>();
|
||
foreach (var (printingId, count) in Seeds)
|
||
deck.AddRange(Enumerable.Repeat(printingId, count));
|
||
|
||
foreach (var group in _catalog.Printings
|
||
.Where(p => !p.IsParallel)
|
||
.GroupBy(p => p.NameId)
|
||
.OrderBy(g => g.Key, StringComparer.Ordinal))
|
||
{
|
||
if (deck.Count >= 50)
|
||
break;
|
||
|
||
if (names.Contains(group.Key))
|
||
continue;
|
||
|
||
var card = _catalog.Card(group.Key);
|
||
if (card.IsSentinel || card.Trigger == TriggerType.Heal)
|
||
continue;
|
||
|
||
var printingId = group.First().PrintingId;
|
||
for (var i = 0; i < 4 && deck.Count < 50; i++)
|
||
deck.Add(printingId);
|
||
}
|
||
|
||
return deck;
|
||
}
|
||
|
||
private static void Give(GameEngine engine, int player, string printingId)
|
||
{
|
||
var p = engine.State.Players[player];
|
||
var index = p.Deck.FindIndex(c => c.PrintingId == printingId);
|
||
Assert.True(index >= 0, $"В колоде нет {printingId}");
|
||
|
||
var card = p.Deck[index];
|
||
p.Deck.RemoveAt(index);
|
||
p.Hand.Add(card);
|
||
}
|
||
|
||
private static void StackDeck(PlayerState player, params string[] printingIds)
|
||
{
|
||
var cards = new List<CardInstance>();
|
||
foreach (var printingId in printingIds)
|
||
{
|
||
var index = player.Deck.FindIndex(c => c.PrintingId == printingId);
|
||
Assert.True(index >= 0, $"В колоде нет {printingId}");
|
||
|
||
cards.Add(player.Deck[index]);
|
||
player.Deck.RemoveAt(index);
|
||
}
|
||
|
||
for (var i = cards.Count - 1; i >= 0; i--)
|
||
player.Deck.Insert(0, cards[i]);
|
||
}
|
||
|
||
private static void KeepOnly(GameEngine engine, int player, string nameId, int keep)
|
||
{
|
||
var p = engine.State.Players[player];
|
||
var matches = p.Deck
|
||
.Where(c => engine.State.Catalog.CardOf(c.PrintingId).NameId == nameId)
|
||
.ToList();
|
||
|
||
foreach (var card in matches.Skip(keep))
|
||
p.Deck.Remove(card);
|
||
}
|
||
|
||
private static void GiveDamage(GameEngine engine, int player, int amount)
|
||
{
|
||
var p = engine.State.Players[player];
|
||
for (var i = 0; i < amount; i++)
|
||
{
|
||
Assert.NotEmpty(p.Deck);
|
||
var card = p.Deck[0];
|
||
p.Deck.RemoveAt(0);
|
||
card.FaceUp = false;
|
||
p.Damage.Add(card);
|
||
}
|
||
}
|
||
|
||
private static void SetVanguard(GameEngine engine, int player, string printingId)
|
||
{
|
||
var p = engine.State.Players[player];
|
||
var index = p.Deck.FindIndex(c => c.PrintingId == printingId);
|
||
Assert.True(index >= 0, $"В колоде нет {printingId}");
|
||
|
||
var card = p.Deck[index];
|
||
p.Deck.RemoveAt(index);
|
||
p.Vanguard = card;
|
||
}
|
||
|
||
private static CardInstance TakeFromDeck(PlayerState player, string printingId)
|
||
{
|
||
var index = player.Deck.FindIndex(c => c.PrintingId == printingId);
|
||
Assert.True(index >= 0, $"В колоде нет {printingId}");
|
||
|
||
var card = player.Deck[index];
|
||
player.Deck.RemoveAt(index);
|
||
return card;
|
||
}
|
||
|
||
private static CardInstance TakeAnyFromDeck(PlayerState player)
|
||
{
|
||
Assert.NotEmpty(player.Deck);
|
||
var card = player.Deck[0];
|
||
player.Deck.RemoveAt(0);
|
||
return card;
|
||
}
|
||
}
|