VanguardReMaster/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs

1223 lines
41 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Vanguard.Rules.Cards;
using Vanguard.Rules.Engine;
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.TemporaryPower);
Assert.True(engine.Execute(new EndTurnCommand()).Accepted);
Assert.Equal(0, vanguard.TemporaryPower);
}
[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.Battle!.AbilityPower);
}
[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.Battle!.AbilityPower);
}
[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.Battle!.AbilityPower);
}
[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.TemporaryPower);
Assert.Equal(handBefore - 1, engine.State.Players[0].Hand.Count);
Assert.Equal(Step.Guard, engine.State.Step);
}
[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;
}
}