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 private const string Mocha = "BT01/018EN"; // OTT, +3000 при четырёх картах в руке private const string HellSpider = "BT01/039EN"; // Megacolony, запрет вставать private const string BrutalJack = "BT01/029EN"; // Nova Grappler, Сдерживание private const string ToughBoy = "BT01/060EN"; // Nova Grappler, grade 1 [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); // второй Kagero, чтобы не срабатывал штраф −2000 engine.State.Players[0].RearGuards[0] = TakeFromDeck(engine.State.Players[0], Nehalem); 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()); } [Fact] public void Grade_Three_Vanguard_Performs_Two_Checks() { var engine = StartedEngine(); engine.Execute(new SkipRideCommand()); SetVanguard(engine, 0, Overlord); StackDeck(engine.State.Players[0], Gallatin, Gallatin); engine.Execute(new EnterBattleCommand()); engine.Execute(new AttackCommand(-1, -1)); engine.Execute(new PassGuardCommand()); Assert.Equal(2, engine.Events.OfType().Count()); } [Fact] public void Twin_Drive_Can_Be_Removed_By_Effect() { var engine = StartedEngine(); engine.Execute(new SkipRideCommand()); SetVanguard(engine, 0, Overlord); engine.State.Players[0].Vanguard!.DriveChecksOverride = 1; StackDeck(engine.State.Players[0], Gallatin, Gallatin); engine.Execute(new EnterBattleCommand()); engine.Execute(new AttackCommand(-1, -1)); engine.Execute(new PassGuardCommand()); Assert.Single(engine.Events.OfType()); } [Fact] public void Rear_Guard_Performs_No_Checks_By_Default() { var engine = ReadyToAttackRearGuard(); engine.Execute(new AttackCommand(3, 0)); engine.Execute(new PassGuardCommand()); Assert.Empty(engine.Events.OfType()); } [Fact] public void Effect_Can_Give_Drive_Check_To_Rear_Guard() { var engine = ReadyToAttackRearGuard(); engine.State.Players[0].RearGuards[3]!.DriveChecksOverride = 1; StackDeck(engine.State.Players[0], Gallatin); engine.Execute(new AttackCommand(3, 0)); engine.Execute(new PassGuardCommand()); Assert.Single(engine.Events.OfType()); } [Fact] public void 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? 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? 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 Mocha_Gets_Power_When_Hand_Has_Four_Cards() { var engine = StartedEngine(); engine.Execute(new SkipRideCommand()); var mocha = MakeCard(Mocha); engine.State.Players[0].Vanguard = mocha; StackDeck(engine.State.Players[0], Gallatin, Gallatin); Assert.True(engine.State.Players[0].Hand.Count >= 4); engine.Execute(new EnterBattleCommand()); engine.Execute(new AttackCommand(-1, -1)); Assert.Equal(3000, mocha.Modifiers.Where(m => m.EffectId == "ability").Sum(m => m.Power)); } [Fact] public void HellSpider_Prevents_Chosen_RearGuard_From_Standing() { var engine = InMainPhase(); GiveDamage(engine, 0, 2); var opponent = engine.State.Players[1]; var target = MakeCard(Gallatin); opponent.RearGuards[0] = target; var spider = MakeCard(HellSpider); engine.State.Players[0].Hand.Add(spider); Assert.True(engine.Execute(new CallCommand(HellSpider, 0)).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.True(engine.State.HasRestriction(target, RestrictionType.CannotStand)); Assert.Equal(2, engine.State.Players[0].Damage.Count(c => c.FaceUp)); } [Fact] public void BrutalJack_Loses_Restraint_And_Gets_Power_From_Nova_Grappler_Boost() { var engine = InMainPhase(); GiveDamage(engine, 0, 1); var jack = MakeCard(BrutalJack); engine.State.Players[0].Hand.Add(jack); engine.Execute(new CallCommand(BrutalJack, 3)); var toughBoy = MakeCard(ToughBoy); engine.State.Players[0].Hand.Add(toughBoy); engine.Execute(new CallCommand(ToughBoy, 0)); jack.Rested = false; toughBoy.Rested = false; Assert.True(engine.State.IsRestraint(jack)); Assert.True(engine.Execute(new ActivateAbilityCommand(jack.Id)).Accepted); Assert.False(engine.State.IsRestraint(jack)); Assert.True(engine.Execute(new EnterBattleCommand()).Accepted); Assert.True(engine.Execute(new AttackCommand(3, -1, 0)).Accepted); Assert.Equal(5000, jack.Modifiers.Where(m => m.EffectId == "ability").Sum(m => m.Power)); } [Fact] public void Overlord_Activation_Adds_Power_Removes_Twin_Drive_And_Stands_On_RearGuard_Hit() { var engine = InMainPhase(); GiveDamage(engine, 0, 3); var overlord = MakeCard(Overlord); engine.State.Players[0].Vanguard = overlord; Assert.True(engine.Execute(new ActivateAbilityCommand(overlord.Id)).Accepted); Assert.Equal(5000, overlord.Modifiers.Where(m => m.EffectId == "dragonic-overlord").Sum(m => m.Power)); Assert.Equal(1, overlord.DriveChecksOverride); Assert.Single(overlord.GrantedAbilities); StackDeck(engine.State.Players[0], Gallatin); engine.Execute(new EnterBattleCommand()); var opponent = engine.State.Players[1]; opponent.RearGuards[0] = MakeCard(Marron); Assert.True(engine.Execute(new AttackCommand(-1, 0)).Accepted); Assert.True(engine.Execute(new PassGuardCommand()).Accepted); Assert.False(overlord.Rested); Assert.Null(opponent.RearGuards[0]); engine.Execute(new EndTurnCommand()); Assert.Null(overlord.DriveChecksOverride); Assert.Empty(overlord.GrantedAbilities); } [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 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 BuildDeck() { var names = Seeds .Select(s => _catalog.CardOf(s.PrintingId).NameId) .ToHashSet(StringComparer.Ordinal); var deck = new List(); 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(); 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; } private static int _nextInstanceId = 20_000; /// Карта, которой нет в тестовой колоде: кладём в руку или на поле вручную. private static CardInstance MakeCard(string printingId) => new() { Id = Interlocked.Increment(ref _nextInstanceId), PrintingId = printingId }; }