using NUnit.Framework; using BaseGames.AI; namespace BaseGames.Tests.EditMode.AI { /// /// BrainGraph 框架健壮性/边界测试:构图校验、转换优先级、全局转换、 /// 事件与条件优先、队列信号、自转换、trace 上限、Blackboard、dt 传递。 /// 与 AiRuntimeTests(主干行为)互补,覆盖易回归的边角。 /// public class AiFrameworkTests { // ── 构图校验(Build 应对错误声明抛异常,尽早暴露)────────────────── [Test] public void Build_Throws_WhenEntryNotSet() { var b = new BrainBuilder(); b.State("A"); Assert.Throws(() => b.Build()); } [Test] public void Build_Throws_WhenEntryStateUndeclared() { var b = new BrainBuilder(); b.Entry("Missing"); b.State("A"); Assert.Throws(() => b.Build()); } // 局部转换指向未声明状态 → BrainBuilderTests.Build_UnknownTransitionTarget_Throws 已覆盖,此处不重复。 [Test] public void Build_Throws_WhenGlobalTransitionTargetUndeclared() { var b = new BrainBuilder(); b.Entry("A"); b.State("A"); b.Global().To("Ghost").OnEvent(AiSignal.Died); Assert.Throws(() => b.Build()); } [Test] public void GlobalBuilder_Throws_WhenOnEventBeforeTo() { var b = new BrainBuilder(); Assert.Throws(() => b.Global().OnEvent(AiSignal.Died)); } [Test] public void GlobalBuilder_Throws_WhenWhenBeforeTo() { var b = new BrainBuilder(); Assert.Throws(() => b.Global().When(c => true, "x")); } // ── 转换优先级 / 全局转换 ───────────────────────────────────────── [Test] public void ConditionTransition_FirstDeclaredWins() { var b = new BrainBuilder(); b.Entry("A"); b.State("A") .To("B").When(c => true, "first") .To("C").When(c => true, "second"); // 两者都真,先声明的 B 胜 b.State("B"); b.State("C"); var rt = new AiRuntime(b.Build(), new FakeAiContext()); rt.Tick(0.1f); Assert.AreEqual("B", rt.CurrentStateName); } [Test] public void GlobalConditionTransition_Fires() { var b = new BrainBuilder(); b.Entry("A"); b.State("A"); b.State("Panic"); b.Global().To("Panic").When(c => c.Vitals.HpBelow(0.3f), "lowHp"); var ctx = new FakeAiContext(); ctx.V.Hp = 0.1f; var rt = new AiRuntime(b.Build(), ctx); rt.Tick(0.1f); Assert.AreEqual("Panic", rt.CurrentStateName); } [Test] public void GlobalConditionTransition_TakesPriorityOverLocal() { var b = new BrainBuilder(); b.Entry("A"); b.State("A").To("B").When(c => true, "local"); b.State("B"); b.State("G"); b.Global().To("G").When(c => true, "global"); // 全局先评估 var rt = new AiRuntime(b.Build(), new FakeAiContext()); rt.Tick(0.1f); Assert.AreEqual("G", rt.CurrentStateName); } // ── 事件 vs 条件优先 / 队列信号 ─────────────────────────────────── [Test] public void EventTransition_TakesPriorityOverCondition_SameTick() { var b = new BrainBuilder(); b.Entry("A"); b.State("A") .To("ByCond").When(c => true, "cond") // 条件恒真 .To("ByEvent").OnEvent(AiSignal.Died); // 事件优先(Tick 第1步) b.State("ByCond"); b.State("ByEvent"); var rt = new AiRuntime(b.Build(), new FakeAiContext()); rt.Send(AiSignal.Died); rt.Tick(0.1f); Assert.AreEqual("ByEvent", rt.CurrentStateName); } [Test] public void UnmatchedSignal_ConsumedThenConditionEvaluated() { var b = new BrainBuilder(); b.Entry("A"); b.State("A").To("B").When(c => c.Sensor.SeesPlayer(), "sees"); b.State("B"); var ctx = new FakeAiContext(); ctx.S.Sees = true; var rt = new AiRuntime(b.Build(), ctx); rt.Send(AiSignal.Died); // A 无 Died 转换 → 被消费掉,不阻塞条件评估 rt.Tick(0.1f); Assert.AreEqual("B", rt.CurrentStateName); } [Test] public void QueuedDuplicateSignals_TransitionOnce_RemainderConsumed() { // 注:原测试用两个不同信号验证"第一个匹配的先转"。AiSignal 现只有 Died 一个值, // 该形态暂不可表达;等将来出现第二个真实信号(如战斗激活)时恢复此覆盖。 // 此处覆盖仍然成立的部分:残留信号被消费掉,不会重复触发转换。 var b = new BrainBuilder(); b.Entry("A"); b.State("A").To("B").OnEvent(AiSignal.Died); b.State("B").OnEnter(c => c.Blackboard.Set("enters", c.Blackboard.Get("enters") + 1)); var ctx = new FakeAiContext(); var rt = new AiRuntime(b.Build(), ctx); rt.Send(AiSignal.Died); rt.Send(AiSignal.Died); rt.Tick(0.1f); Assert.AreEqual("B", rt.CurrentStateName); rt.Tick(0.1f); Assert.AreEqual("B", rt.CurrentStateName); Assert.AreEqual(1, ctx.BB.Get("enters")); // 残留信号被消费,未重入 B } // ── 自转换 / OnTick 时序 ────────────────────────────────────────── [Test] public void ConditionSelfTransition_DoesNotReenter() { var b = new BrainBuilder(); b.Entry("A"); b.State("A") .OnEnter(c => c.Blackboard.Set("enters", c.Blackboard.Get("enters") + 1)) .To("A").When(c => true, "self"); // 自转换 var ctx = new FakeAiContext(); var rt = new AiRuntime(b.Build(), ctx); // 构造进入 → enters=1 rt.Tick(0.1f); // 自转换应 no-op,不重跑 OnEnter Assert.AreEqual(1, ctx.BB.Get("enters")); Assert.AreEqual("A", rt.CurrentStateName); } [Test] public void TransitionWins_SkipsOnTick_SameFrame() { var b = new BrainBuilder(); b.Entry("A"); b.State("A") .Tick(c => c.Blackboard.Set("ticked", true)) .To("B").When(c => true, "go"); b.State("B"); var ctx = new FakeAiContext(); var rt = new AiRuntime(b.Build(), ctx); rt.Tick(0.1f); // 转换发生 → 当帧不应执行 A 的 OnTick Assert.AreEqual("B", rt.CurrentStateName); Assert.IsFalse(ctx.BB.Has("ticked")); } [Test] public void Tick_TwoArg_ReceivesDeltaTime() { var b = new BrainBuilder(); b.Entry("A"); b.State("A").Tick((c, dt) => c.Blackboard.Set("dt", dt)); // 无转换 var ctx = new FakeAiContext(); var rt = new AiRuntime(b.Build(), ctx); rt.Tick(0.25f); Assert.AreEqual(0.25f, ctx.BB.Get("dt"), 1e-5f); } // ── trace 上限 / Blackboard / 挂起标志 ──────────────────────────── [Test] public void Trace_CapsAtSixteen() { var b = new BrainBuilder(); b.Entry("A"); b.State("A").To("B").When(c => true, "toB"); b.State("B").To("A").When(c => true, "toA"); // A↔B 每帧一次转换 var rt = new AiRuntime(b.Build(), new FakeAiContext()); for (int i = 0; i < 20; i++) rt.Tick(0.1f); Assert.AreEqual(16, rt.Trace.Count); // 环形缓冲上限 16 } // Blackboard 基础 Set/Get/Has/Clear/Snapshot → BlackboardTests 已覆盖,此处不重复。 [Test] public void IsSuspended_ClearedAfterEventWhileNotControllable() { var b = new BrainBuilder(); b.Entry("A"); b.State("A"); b.State("Dead"); b.Global().To("Dead").OnEvent(AiSignal.Died); var ctx = new FakeAiContext(); ctx.V.Controllable = false; // 受击中(若无事件本会挂起) var rt = new AiRuntime(b.Build(), ctx); rt.Send(AiSignal.Died); rt.Tick(0.1f); Assert.AreEqual("Dead", rt.CurrentStateName); Assert.IsFalse(rt.IsSuspended); // 事件推进转换后清挂起 } [Test] public void State_Redeclaration_AccumulatesTransitions() { var b = new BrainBuilder(); b.Entry("A"); b.State("A").To("B").When(c => false, "t1"); // 第一次声明 b.State("A").To("C").When(c => true, "t2"); // 再次 State("A") 应取同一状态,累加转换 b.State("B"); b.State("C"); var graph = b.Build(); Assert.AreEqual(2, graph.GetState("A").Transitions.Count); var rt = new AiRuntime(graph, new FakeAiContext()); rt.Tick(0.1f); // t1 假、t2 真 → C Assert.AreEqual("C", rt.CurrentStateName); } } }