From a1e7c250bf25cf2ef6eab0ed116275376791a962 Mon Sep 17 00:00:00 2001 From: MpakobecEx Date: Wed, 21 Oct 2026 13:14:16 +0500 Subject: [PATCH] Target selection and Activated abilities PendingChoice/ChooseCommand let effects ask the player to pick a unit. Added Counter Blast and Soul Blast cost helpers. Blaster Blade implemented for both zones with its CB2 cost and rear-guard target. Activated abilities pay rest-self and discard; Lake Maiden Lien, Gojo and Shout draw a card. Seven tests. --- IMPLEMENTATION_CHECKLIST.md | 4 +- .../Engine/Abilities/Abilities.cs | 20 ++ .../Engine/Abilities/CardScripts.cs | 92 +++++++ .../src/Vanguard.Rules/Engine/GameCommands.cs | 2 + .../src/Vanguard.Rules/Engine/GameEngine.cs | 240 +++++++++++++++++- .../src/Vanguard.Rules/Engine/GameModels.cs | 16 ++ server/src/Vanguard.Rules/Engine/GameState.cs | 3 + .../Vanguard.Rules.Tests/GameEngineTests.cs | 116 +++++++++ 8 files changed, 479 insertions(+), 14 deletions(-) diff --git a/IMPLEMENTATION_CHECKLIST.md b/IMPLEMENTATION_CHECKLIST.md index ba5cfc0..8bf8778 100644 --- a/IMPLEMENTATION_CHECKLIST.md +++ b/IMPLEMENTATION_CHECKLIST.md @@ -114,9 +114,9 @@ - [ ] Discard, Shuffle как отдельные интенты. - [x] Число проверок принадлежит атакующей карте: 0–2 ранг — одна, 3+ — две; Арьергард по умолчанию без проверок; `DriveChecksOverride` меняет число (в том числе снимает двойную). - [x] Результат боя: `Hit` / `Guarded` как событие. -- [x] Событийная шина: `Triggered` (OnRideOver, OnBoost), `Continuous` с пересчётом при запросе; `Activated` ещё нет. +- [x] Событийная шина: `Triggered` (OnRideOver, OnPlace, OnBoost), `Continuous` с пересчётом при запросе, `Activated` (повернуть себя + сброс карты). - [x] Check Timing: сбор сработавших, порядок (активный игрок первым, выбор игрока), пауза на опциональные способности. -- [ ] `Activated`-способности и выбор целей игроком. +- [x] Выбор целей: `PendingChoice` + `ChooseCommand`, используется Blaster Blade для уничтожения Арьергарда противника. - [ ] Стоимости: CB, SB, discard, rest self, retire self, put to soul, return to deck. - [ ] Цели: авто по фильтру и выбор игроком, «до N» / «ровно N». - [~] Слои модификаторов силы/крита: усиления от триггеров живут до конца хода, постоянных слоёв нет. diff --git a/server/src/Vanguard.Rules/Engine/Abilities/Abilities.cs b/server/src/Vanguard.Rules/Engine/Abilities/Abilities.cs index 307eeda..05ee3d7 100644 --- a/server/src/Vanguard.Rules/Engine/Abilities/Abilities.cs +++ b/server/src/Vanguard.Rules/Engine/Abilities/Abilities.cs @@ -5,6 +5,7 @@ namespace Vanguard.Rules.Engine.Abilities; public enum AbilityTrigger { OnRideOver, + OnPlace, OnBoost, OnPhaseStart, OnAttackDeclared, @@ -33,6 +34,12 @@ public sealed class AbilityDefinition public Func? Condition { get; init; } public required Action Resolve { get; init; } + /// Проверка, что стоимость можно оплатить. + public Func? CanPay { get; init; } + + /// Оплата стоимости перед разрешением. + public Action? Pay { get; init; } + /// «Вы можете» — движок спрашивает игрока перед разрешением. public bool Optional { get; init; } } @@ -45,11 +52,24 @@ public sealed class ContinuousAbility public Func? CriticalBonus { get; init; } } +/// +/// Активируемая способность: игрок сам объявляет её в Основной фазе. +/// Стоимость (повернуть себя, сбросить карту) оплачивает движок по описанию. +/// +public sealed class ActivatedAbility +{ + public required Func CanActivate { get; init; } + public bool RestsSelf { get; init; } + public bool RequiresDiscard { get; init; } + public required Action Resolve { get; init; } +} + public abstract class CardScript { public abstract string NameId { get; } public virtual IEnumerable Abilities => Array.Empty(); public virtual IEnumerable Continuous => Array.Empty(); + public virtual IEnumerable Activated => Array.Empty(); } /// Сработавшая способность, ожидающая выбора порядка или решения игрока. diff --git a/server/src/Vanguard.Rules/Engine/Abilities/CardScripts.cs b/server/src/Vanguard.Rules/Engine/Abilities/CardScripts.cs index 7c453d1..df0106a 100644 --- a/server/src/Vanguard.Rules/Engine/Abilities/CardScripts.cs +++ b/server/src/Vanguard.Rules/Engine/Abilities/CardScripts.cs @@ -11,9 +11,101 @@ public static class CardScripts ["barcgal"] = new BarcgalScript(), ["wingal"] = new WingalScript(), ["king-of-knights-alfred"] = new AlfredScript(), + ["blaster-blade"] = new BlasterBladeScript(), + ["lake-maiden-lien"] = new DrawOneScript("lake-maiden-lien"), + ["dragon-monk-gojo"] = new DrawOneScript("dragon-monk-gojo"), + ["screamin-and-dancin-announcer-shout"] = new DrawOneScript("screamin-and-dancin-announcer-shout"), }; } +/// +/// «[ACT]: повернуть эту карту и сбросить карту из руки → взять карту». +/// Общий скрипт для Lake Maiden, Lien, Dragon Monk, Gojo и Shout. +/// +public sealed class DrawOneScript : CardScript +{ + public DrawOneScript(string nameId) => NameId = nameId; + + public override string NameId { get; } + + public override IEnumerable Activated + { + get + { + yield return new ActivatedAbility + { + CanActivate = _ => true, + RestsSelf = true, + RequiresDiscard = true, + Resolve = ctx => ctx.Engine.DrawCards(ctx.Owner, 1), + }; + } + } +} + +/// +/// Blaster Blade: при размещении оплатить Counter Blast 2 и уничтожить Арьергард противника. +/// На Авангарде — любой, на Арьергарде — только 2 ранга или выше и при Авангарде Royal Paladin. +/// +public sealed class BlasterBladeScript : CardScript +{ + public override string NameId => "blaster-blade"; + + public override IEnumerable Abilities + { + get + { + yield return new AbilityDefinition + { + Trigger = AbilityTrigger.OnPlace, + Optional = true, + Condition = ctx => ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner), + CanPay = ctx => ctx.Engine.CanCounterBlast(ctx.Owner, 2), + Pay = ctx => ctx.Engine.CounterBlast(ctx.Owner, 2), + Resolve = ctx => + { + var opponent = 1 - ctx.Owner; + var slots = ctx.Engine.RearGuardSlots(opponent, _ => true); + if (slots.Count == 0) + return; + + ctx.Engine.AskUnitChoice( + ctx.Owner, + opponent, + "Выберите Арьергард противника", + slots, + slot => ctx.Engine.RetireRearGuard(opponent, slot)); + }, + }; + + yield return new AbilityDefinition + { + Trigger = AbilityTrigger.OnPlace, + Optional = true, + Condition = ctx => + !ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner) && + ctx.Engine.VanguardClan(ctx.Owner) == Clan.RoyalPaladin, + CanPay = ctx => ctx.Engine.CanCounterBlast(ctx.Owner, 2), + Pay = ctx => ctx.Engine.CounterBlast(ctx.Owner, 2), + Resolve = ctx => + { + var opponent = 1 - ctx.Owner; + var slots = ctx.Engine.RearGuardSlots(opponent, d => d.Grade >= 2); + if (slots.Count == 0) + return; + + ctx.Engine.AskUnitChoice( + ctx.Owner, + opponent, + "Выберите Арьергард противника 2 ранга или выше", + slots, + slot => ctx.Engine.RetireRearGuard(opponent, slot)); + }, + }; + } + } +} + /// /// Barcgal: когда другой Royal Paladin сменяет эту карту, вы можете призвать её в круг Арьергарда. /// diff --git a/server/src/Vanguard.Rules/Engine/GameCommands.cs b/server/src/Vanguard.Rules/Engine/GameCommands.cs index 362b13f..821d98a 100644 --- a/server/src/Vanguard.Rules/Engine/GameCommands.cs +++ b/server/src/Vanguard.Rules/Engine/GameCommands.cs @@ -16,6 +16,8 @@ public sealed record PassGuardCommand : GameCommand; public sealed record ResolveTriggerCommand(int PowerTarget, int EffectTarget) : GameCommand; public sealed record SelectTriggerCommand(int Index) : GameCommand; public sealed record ResolveAbilityCommand(bool Accept) : GameCommand; +public sealed record ChooseCommand(int Index) : GameCommand; +public sealed record ActivateAbilityCommand(int InstanceId, string? DiscardPrintingId = null) : GameCommand; public sealed record EndTurnCommand : GameCommand; public sealed record CommandResult(bool Accepted, string? Reason) diff --git a/server/src/Vanguard.Rules/Engine/GameEngine.cs b/server/src/Vanguard.Rules/Engine/GameEngine.cs index fa38751..e31d309 100644 --- a/server/src/Vanguard.Rules/Engine/GameEngine.cs +++ b/server/src/Vanguard.Rules/Engine/GameEngine.cs @@ -18,9 +18,8 @@ namespace Vanguard.Rules.Engine; /// из заднего ряда — только если эффект разрешил (InterceptFromBackRow); /// * число проверок принадлежит атакующей карте: 0–2 ранг — одна, 3+ — две; /// Арьергард по умолчанию без проверок, но эффект может их выдать; -/// * способности карт подключаются скриптами (CardScript): Triggered и Continuous; -/// Check Timing собирает сработавшие, активный игрок выбирает порядок, -/// опциональные способности ждут ResolveAbilityCommand; +/// * способности карт: Triggered, Continuous и Activated; выбор цели идёт через +/// PendingChoice (ChooseCommand), порядок триггеров выбирает активный игрок; /// * непрерывные эффекты пересчитываются при каждом запросе (EffectivePower); /// * триггер требует выбора целей через ResolveTriggerCommand: /// +5000 любой карте в кругах Авангарда или Арьергарда, затем второй эффект; @@ -115,9 +114,15 @@ public sealed class GameEngine private void OfferAbility(TriggeredAbility ability) { - _events.Add(new AbilityTriggeredEvent(ability.Owner, ability.Context.Source.PrintingId, ability.Definition.Trigger)); + var definition = ability.Definition; - if (ability.Definition.Optional) + // если стоимость оплатить нельзя, способность не предлагается + if (definition.CanPay is not null && !definition.CanPay(ability.Context)) + return; + + _events.Add(new AbilityTriggeredEvent(ability.Owner, ability.Context.Source.PrintingId, definition.Trigger)); + + if (definition.Optional) { State.PendingAbility = ability; State.Step = Step.Ability; @@ -131,26 +136,126 @@ public sealed class GameEngine private void ResolveAbility(TriggeredAbility ability, bool accepted) { if (accepted) + { + ability.Definition.Pay?.Invoke(ability.Context); ability.Definition.Resolve(ability.Context); + } _events.Add(new AbilityResolvedEvent(ability.Owner, ability.Context.Source.PrintingId, accepted)); + // способность попросила выбрать цель — ждём ChooseCommand + if (State.Step == Step.Choice) + return; + + ContinueOrder(); + } + + private void ContinueOrder() + { if (State.PendingOrder is { Count: > 0 } order) { - var next = order[0]; - order.RemoveAt(0); - - if (order.Count == 0) + if (order.Count == 1) + { + var next = order[0]; State.PendingOrder = null; + State.ActivePlayer = next.Owner; + OfferAbility(next); + return; + } - State.ActivePlayer = next.Owner; - OfferAbility(next); + State.Step = Step.Ability; + State.ActivePlayer = order[0].Owner; return; } State.Step = Step.None; } + private CommandResult Activate(int instanceId, string? discardPrintingId) + { + if (State.Phase != Phase.Main) + return CommandResult.Reject("Активируемые способности доступны только в Основной фазе."); + + if (State.Step != Step.None || State.PendingAbility is not null || State.PendingChoice is not null) + return CommandResult.Reject("Сейчас нельзя активировать способности."); + + var player = State.ActivePlayer; + var p = State.Players[player]; + var unit = AllUnits(player).FirstOrDefault(u => u.Id == instanceId); + if (unit is null) + return CommandResult.Reject("Карта не на поле."); + + var nameId = State.Catalog.CardOf(unit.PrintingId).NameId; + if (!_scripts.TryGetValue(nameId, out var script)) + return CommandResult.Reject("У карты нет способностей."); + + var context = new AbilityContext + { + Engine = this, + Source = unit, + Owner = player, + Phase = State.Phase, + }; + + var ability = script.Activated.FirstOrDefault(a => a.CanActivate(context)); + if (ability is null) + return CommandResult.Reject("Нет доступной способности."); + + if (ability.RestsSelf && unit.Rested) + return CommandResult.Reject("Карта уже повёрнута."); + + CardInstance? discard = null; + if (ability.RequiresDiscard) + { + if (discardPrintingId is null) + return CommandResult.Reject("Нужно выбрать карту для сброса."); + + var index = p.Hand.FindIndex(c => c.PrintingId == discardPrintingId); + if (index < 0) + return CommandResult.Reject("Карты для сброса нет в руке."); + + discard = p.Hand[index]; + } + + if (ability.RestsSelf) + unit.Rested = true; + + if (discard is not null) + { + p.Hand.Remove(discard); + p.Drop.Add(discard); + } + + ability.Resolve(context); + _events.Add(new AbilityResolvedEvent(player, unit.PrintingId, true)); + + return CommandResult.Ok(); + } + + private CommandResult Choose(int index) + { + var choice = State.PendingChoice; + if (choice is null || State.Step != Step.Choice) + return CommandResult.Reject("Сейчас нет выбора."); + + if (State.ActivePlayer != choice.Player) + return CommandResult.Reject("Сейчас выбирает другой игрок."); + + if (index < 0 || index >= choice.Options.Count) + return CommandResult.Reject("Некорректный вариант выбора."); + + var option = choice.Options[index]; + State.PendingChoice = null; + State.Step = Step.None; + + choice.Resolve(option.Slot); + + if (State.Step != Step.Choice) + ContinueOrder(); + + return CommandResult.Ok(); + } + private CommandResult SelectTrigger(int index) { var order = State.PendingOrder; @@ -261,6 +366,107 @@ public sealed class GameEngine battle.AbilityPower += amount; } + // ---------- стоимости ---------- + + public int FaceDownDamage(int player) => + State.Players[player].Damage.Count(c => !c.FaceUp); + + public bool CanCounterBlast(int player, int amount) => + FaceDownDamage(player) >= amount; + + public void CounterBlast(int player, int amount) + { + var flipped = 0; + foreach (var card in State.Players[player].Damage) + { + if (flipped == amount) + break; + + if (card.FaceUp) + continue; + + card.FaceUp = true; + flipped++; + } + } + + public bool CanSoulBlast(int player, int amount) => + State.Players[player].Soul.Count >= amount; + + public void SoulBlast(int player, int amount) + { + var soul = State.Players[player].Soul; + for (var i = 0; i < amount && soul.Count > 0; i++) + { + var card = soul[0]; + soul.RemoveAt(0); + State.Players[player].Drop.Add(card); + } + } + + // ---------- выбор целей ---------- + + /// Круги Арьергарда игрока, подходящие под фильтр. + public IReadOnlyList RearGuardSlots(int player, Func predicate) + { + var slots = new List(); + for (var slot = 0; slot < PlayerState.RearGuardSlots; slot++) + { + var unit = State.Players[player].RearGuards[slot]; + if (unit is null) + continue; + + if (predicate(State.Catalog.CardOf(unit.PrintingId))) + slots.Add(slot); + } + + return slots; + } + + /// Останавливает разрешение и просит игрока выбрать карту на поле. + public void AskUnitChoice( + int chooser, + int targetPlayer, + string prompt, + IReadOnlyList slots, + Action onChosen) + { + var options = slots + .Select(slot => + { + var unit = State.GetUnit(targetPlayer, slot)!; + return new ChoiceOption(slot, State.Catalog.CardOf(unit.PrintingId).NameEn); + }) + .ToList(); + + State.PendingChoice = new PendingChoice + { + Player = chooser, + Prompt = prompt, + Options = options, + Resolve = onChosen, + }; + State.Step = Step.Choice; + State.ActivePlayer = chooser; + } + + public void RetireRearGuard(int player, int slot) + { + var unit = State.Players[player].RearGuards[slot]; + if (unit is null) + return; + + State.Players[player].RearGuards[slot] = null; + State.Players[player].Drop.Add(unit); + _events.Add(new UnitRetiredEvent(player, slot)); + } + + public Clan VanguardClan(int player) + { + var vanguard = State.Players[player].Vanguard; + return vanguard is null ? Clan.RoyalPaladin : State.Catalog.CardOf(vanguard.PrintingId).Clan; + } + // ---------- старт партии ---------- public void Start() @@ -332,6 +538,8 @@ public sealed class GameEngine ResolveTriggerCommand c => ResolveTrigger(c), SelectTriggerCommand c => SelectTrigger(c.Index), ResolveAbilityCommand c => ResolvePendingAbility(c.Accept), + ChooseCommand c => Choose(c.Index), + ActivateAbilityCommand c => Activate(c.InstanceId, c.DiscardPrintingId), EndTurnCommand => EndTurn(), _ => CommandResult.Reject("Неизвестная команда."), }; @@ -404,7 +612,10 @@ public sealed class GameEngine State.Phase = Phase.Main; _events.Add(new PhaseChangedEvent(State.ActivePlayer, Phase.Main)); - TriggerFromCard(State.ActivePlayer, riddenOver, AbilityTrigger.OnRideOver, card); + TriggerFromCard(State.ActivePlayer, card, AbilityTrigger.OnPlace, null); + + if (State.Step == Step.None) + TriggerFromCard(State.ActivePlayer, riddenOver, AbilityTrigger.OnRideOver, card); return CommandResult.Ok(); } @@ -441,6 +652,9 @@ public sealed class GameEngine player.RearGuards[slot] = card; _events.Add(new UnitCalledEvent(State.ActivePlayer, slot, card.PrintingId)); + + TriggerFromCard(State.ActivePlayer, card, AbilityTrigger.OnPlace, null); + return CommandResult.Ok(); } @@ -1050,6 +1264,8 @@ public sealed class GameEngine return card; } + public void DrawCards(int player, int count) => Draw(player, count); + private void Draw(int player, int count) { for (var i = 0; i < count; i++) diff --git a/server/src/Vanguard.Rules/Engine/GameModels.cs b/server/src/Vanguard.Rules/Engine/GameModels.cs index cde0617..4a55de8 100644 --- a/server/src/Vanguard.Rules/Engine/GameModels.cs +++ b/server/src/Vanguard.Rules/Engine/GameModels.cs @@ -20,6 +20,22 @@ public enum Step Drive, Trigger, Ability, + Choice, +} + +/// Вариант выбора, который предлагается игроку. +public sealed record ChoiceOption(int Slot, string Label); + +/// +/// Ожидание выбора игрока. Resolve получает индекс выбранного варианта. +/// Пока выбор не сделан, движок не продолжает разрешение способности. +/// +public sealed class PendingChoice +{ + public required int Player { get; init; } + public required string Prompt { get; init; } + public required IReadOnlyList Options { get; init; } + public required Action Resolve { get; init; } } public enum TriggerContext diff --git a/server/src/Vanguard.Rules/Engine/GameState.cs b/server/src/Vanguard.Rules/Engine/GameState.cs index 686bcde..c9762ec 100644 --- a/server/src/Vanguard.Rules/Engine/GameState.cs +++ b/server/src/Vanguard.Rules/Engine/GameState.cs @@ -60,6 +60,9 @@ public sealed class GameState /// Сработавшие способности ждут выбора порядка. public List? PendingOrder { get; internal set; } + /// Ожидание выбора цели или карты. + public PendingChoice? PendingChoice { get; internal set; } + public bool IsFinished { get; internal set; } public int? Winner { get; internal set; } diff --git a/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs b/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs index 4ae6494..dc73f9d 100644 --- a/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs +++ b/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs @@ -21,6 +21,7 @@ public class GameEngineTests 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 [Fact] public void Start_Places_Starting_Vanguard_And_Draws_Five() @@ -624,6 +625,107 @@ public class GameEngineTests 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 ResolveTrigger_Rejects_When_Nothing_Is_Pending() { @@ -760,6 +862,7 @@ public class GameEngineTests (Nike, 2), (Flogal, 2), (Govannon, 2), (Iseult, 2), (Nehalem, 2), (Overlord, 2), (Conroe, 2), (BlasterBlade, 2), (Wingal, 2), + (Lien, 2), }; private List BuildDeck() @@ -822,6 +925,19 @@ public class GameEngineTests player.Deck.Insert(0, cards[i]); } + 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];