diff --git a/src/SmokeTests.cs b/src/SmokeTests.cs --- a/src/SmokeTests.cs +++ b/src/SmokeTests.cs @@ -40,6 +40,7 @@ Test_StrikePlus_Deals9, Test_Offering_GivesEnergyAndDraw, Test_Hellraiser_AutoplaysDrawnStrikes, + Test_AscendersBane_NotPlayedByPolicies, }; var results = new List(); @@ -124,6 +125,39 @@ return $"expected Vulnerable on dummy, got [{string.Join(",", h.Dummy.Powers.Select(p => p.Id.Entry))}]"; return null; }); + + private static Task Test_AscendersBane_NotPlayedByPolicies() + { + // Ascenders Bane has CardKeyword.Unplayable. Even if a policy considers + // every card in hand, none of the policies should ever choose it. + var h = Harness.BeginCombat(deckOverride: new List + { + typeof(AscendersBane), + typeof(StrikeIronclad), + }); + try + { + var pcs = h.Player.PlayerCombatState!; + Reflect.SetEnergy(pcs, 3); + foreach (var c in pcs.DrawPile.Cards.ToList()) { pcs.DrawPile.RemoveInternal(c); pcs.Hand.AddInternal(c); } + + var policies = new IPlayPolicy[] + { + new GreedyAttackPolicy(), + new HighestDamagePolicy(), + new RandomPolicy(), + new EpsilonGreedyPolicy(new HighestDamagePolicy(), 1.0), + }; + foreach (var p in policies) + { + var pick = p.ChooseCard(h, 3, new Random(0)); + if (pick is AscendersBane) + return Task.FromResult(new TestResult("Policies skip Unplayable cards", false, $"{p.Name} chose Ascenders Bane")); + } + return Task.FromResult(new TestResult("Policies skip Unplayable cards", true, null)); + } + finally { Harness.EndCombat(); } + } private static async Task Test_Hellraiser_AutoplaysDrawnStrikes() { diff --git a/src/Policies/EpsilonGreedyPolicy.cs b/src/Policies/EpsilonGreedyPolicy.cs --- a/src/Policies/EpsilonGreedyPolicy.cs +++ b/src/Policies/EpsilonGreedyPolicy.cs @@ -27,9 +27,7 @@ { if (rng.NextDouble() < _epsilon) { - var affordable = h.Player.PlayerCombatState!.Hand.Cards - .Where(c => c.EnergyCost.GetResolved() <= energyLeft) - .ToList(); + var affordable = Playable.InHand(h, energyLeft).ToList(); if (affordable.Count == 0) return null; return affordable[rng.Next(affordable.Count)]; } diff --git a/src/Policies/GreedyAttackPolicy.cs b/src/Policies/GreedyAttackPolicy.cs --- a/src/Policies/GreedyAttackPolicy.cs +++ b/src/Policies/GreedyAttackPolicy.cs @@ -11,10 +11,9 @@ public CardModel? ChooseCard(Harness.CombatHarness h, int energyLeft, Random rng) { - foreach (var card in h.Player.PlayerCombatState!.Hand.Cards) + foreach (var card in Playable.InHand(h, energyLeft)) { if (card.Type != CardType.Attack) continue; - if (card.EnergyCost.GetResolved() > energyLeft) continue; return card; } return null; diff --git a/src/Policies/HighestDamagePolicy.cs b/src/Policies/HighestDamagePolicy.cs --- a/src/Policies/HighestDamagePolicy.cs +++ b/src/Policies/HighestDamagePolicy.cs @@ -16,11 +16,8 @@ public CardModel? ChooseCard(Harness.CombatHarness h, int energyLeft, Random rng) { - var affordable = h.Player.PlayerCombatState!.Hand.Cards - .Where(c => c.EnergyCost.GetResolved() <= energyLeft) - .ToList(); + var affordable = Playable.InHand(h, energyLeft).ToList(); if (affordable.Count == 0) return null; - return affordable.OrderByDescending(EstimatedDamage).First(); } diff --git a/src/Policies/IPlayPolicy.cs b/src/Policies/IPlayPolicy.cs --- a/src/Policies/IPlayPolicy.cs +++ b/src/Policies/IPlayPolicy.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Linq; +using MegaCrit.Sts2.Core.Entities.Cards; using MegaCrit.Sts2.Core.Models; namespace StS2Sim; @@ -12,4 +15,22 @@ string Name { get; } /// Pick a card to play, or null to end turn. CardModel? ChooseCard(Harness.CombatHarness h, int energyLeft, Random rng); +} + +/// +/// Shared filtering logic so all policies treat "this card is playable right now" +/// identically. Excludes cards with the Unplayable keyword (Ascenders Bane, +/// Burn, etc.) and cards we can't afford. +/// +internal static class Playable +{ + public static bool IsPlayable(CardModel card, int energyLeft) + { + if (card.Keywords.Contains(CardKeyword.Unplayable)) return false; + if (card.EnergyCost.GetResolved() > energyLeft) return false; + return true; + } + + public static IEnumerable InHand(Harness.CombatHarness h, int energyLeft) + => h.Player.PlayerCombatState!.Hand.Cards.Where(c => IsPlayable(c, energyLeft)); } diff --git a/src/Policies/RandomPolicy.cs b/src/Policies/RandomPolicy.cs --- a/src/Policies/RandomPolicy.cs +++ b/src/Policies/RandomPolicy.cs @@ -22,9 +22,7 @@ { if (_earlyEndProbability > 0 && rng.NextDouble() < _earlyEndProbability) return null; - var affordable = h.Player.PlayerCombatState!.Hand.Cards - .Where(c => c.EnergyCost.GetResolved() <= energyLeft) - .ToList(); + var affordable = Playable.InHand(h, energyLeft).ToList(); if (affordable.Count == 0) return null; return affordable[rng.Next(affordable.Count)]; }