using System.Collections.Generic; using NUnit.Framework; using BaseGames.Enemies.Abilities; namespace BaseGames.Tests.EditMode.Enemies { public class EnemyAttackSelectorTests { sealed class FakeCandidate : IAttackCandidate { public bool CanUseV = true, InRange = true, ReqLOS = false, ReqGround = false, Executed; public float WeightV = 1f; public int PriorityV = 0; public bool CanUse => CanUseV; public bool RequiresLineOfSight => ReqLOS; public bool RequiresGrounded => ReqGround; public float Weight => WeightV; public int Priority => PriorityV; public bool InAttackRange() => InRange; public bool Execute() { Executed = true; return true; } } static EnemyAttackSelector Sel(params IAttackCandidate[] cs) => new EnemyAttackSelector(new List(cs)); [Test] public void Empty_NoEligible() { var s = Sel(); Assert.IsFalse(s.HasEligible(true, true)); Assert.IsNull(s.Select(true, true, AttackSelectionMode.Priority)); } [Test] public void ExcludesOnCooldown() { var c = new FakeCandidate { CanUseV = false }; Assert.IsFalse(Sel(c).HasEligible(true, true)); } [Test] public void ExcludesOutOfRange() { var c = new FakeCandidate { InRange = false }; Assert.IsFalse(Sel(c).HasEligible(true, true)); } [Test] public void ExcludesWhenNeedsLOS_ButNoLOS() { var c = new FakeCandidate { ReqLOS = true }; Assert.IsFalse(Sel(c).HasEligible(false, true)); Assert.IsTrue (Sel(c).HasEligible(true, true)); } [Test] public void ExcludesWhenNeedsGrounded_ButAirborne() { var c = new FakeCandidate { ReqGround = true }; Assert.IsFalse(Sel(c).HasEligible(true, false)); Assert.IsTrue (Sel(c).HasEligible(true, true)); } [Test] public void Priority_PicksHighest() { var lo = new FakeCandidate { PriorityV = 1 }; var hi = new FakeCandidate { PriorityV = 5 }; var pick = Sel(lo, hi).Select(true, true, AttackSelectionMode.Priority); Assert.AreSame(hi, pick); } [Test] public void Priority_TieTakesFirstInList() { var a = new FakeCandidate { PriorityV = 3 }; var b = new FakeCandidate { PriorityV = 3 }; var pick = Sel(a, b).Select(true, true, AttackSelectionMode.Priority); Assert.AreSame(a, pick); } [Test] public void Priority_SkipsIneligible() { var blocked = new FakeCandidate { PriorityV = 9, InRange = false }; var ok = new FakeCandidate { PriorityV = 1 }; var pick = Sel(blocked, ok).Select(true, true, AttackSelectionMode.Priority); Assert.AreSame(ok, pick); } [Test] public void Weighted_SingleEligible_AlwaysThatOne() { var only = new FakeCandidate { WeightV = 2f }; for (int i = 0; i < 20; i++) Assert.AreSame(only, Sel(only).Select(true, true, AttackSelectionMode.WeightedRandom)); } [Test] public void Weighted_NeverPicksIneligible() { var bad = new FakeCandidate { WeightV = 100f, InRange = false }; var ok = new FakeCandidate { WeightV = 1f }; for (int i = 0; i < 50; i++) Assert.AreSame(ok, Sel(bad, ok).Select(true, true, AttackSelectionMode.WeightedRandom)); } // ── WeightedRandomAntiRepeat ───────────────────────────────────────── static EnemyAttackSelector SelAntiRepeat(float factor, params IAttackCandidate[] cs) => new EnemyAttackSelector(new List(cs), factor); [Test] public void AntiRepeat_ZeroFactor_NeverPicksSameTwiceWhenAlternativeExists() { var a = new FakeCandidate { WeightV = 1f }; var b = new FakeCandidate { WeightV = 1f }; var s = SelAntiRepeat(0f, a, b); var prev = s.Select(true, true, AttackSelectionMode.WeightedRandomAntiRepeat); for (int i = 0; i < 100; i++) { var cur = s.Select(true, true, AttackSelectionMode.WeightedRandomAntiRepeat); Assert.AreNotSame(prev, cur, "系数为 0 时不应连续选中同一招"); prev = cur; } } [Test] public void AntiRepeat_ZeroFactor_SingleCandidate_StillReturnsIt() { // 只有一招时折扣会把权重压到 0,WeightedPick 返回 -1; // 必须退化为 Priority 路径把它选出来,否则敌人永远不出手。 var only = new FakeCandidate { WeightV = 1f }; var s = SelAntiRepeat(0f, only); for (int i = 0; i < 10; i++) Assert.AreSame(only, s.Select(true, true, AttackSelectionMode.WeightedRandomAntiRepeat)); } [Test] public void AntiRepeat_DiscountsPreviousPick_ButDoesNotBanIt() { // 系数 0.5:a 被选过后权重折半,但仍有机会再被选中。 var a = new FakeCandidate { WeightV = 1f }; var b = new FakeCandidate { WeightV = 1f }; var s = SelAntiRepeat(0.5f, a, b); s.Select(true, true, AttackSelectionMode.WeightedRandomAntiRepeat); int repeats = 0; var prev = s.LastPicked; for (int i = 0; i < 400; i++) { var cur = s.Select(true, true, AttackSelectionMode.WeightedRandomAntiRepeat); if (ReferenceEquals(cur, prev)) repeats++; prev = cur; } Assert.Greater(repeats, 0, "折扣不应等于禁用"); Assert.Less (repeats, 200, "折扣后重复率应低于无折扣时的约 50%"); } [Test] public void AntiRepeat_IneligiblePrevious_DoesNotBlockOthers() { var a = new FakeCandidate { WeightV = 1f }; var b = new FakeCandidate { WeightV = 1f }; var s = SelAntiRepeat(0f, a, b); var first = s.Select(true, true, AttackSelectionMode.WeightedRandomAntiRepeat); ((FakeCandidate)first).CanUseV = false; // 上一招进冷却 var second = s.Select(true, true, AttackSelectionMode.WeightedRandomAntiRepeat); Assert.IsNotNull(second); Assert.AreNotSame(first, second); } [Test] public void PlainWeighted_DoesNotApplyAntiRepeat() { // WeightedRandom 模式下唯一候选恒被选中(回归:折扣不得泄漏到旧模式) var only = new FakeCandidate { WeightV = 1f }; var s = SelAntiRepeat(0f, only); for (int i = 0; i < 10; i++) Assert.AreSame(only, s.Select(true, true, AttackSelectionMode.WeightedRandom)); } } }