using NUnit.Framework; using BaseGames.AI; namespace BaseGames.Tests.EditMode.AI { public class AiRuntimeTests { static AiGraph Graph() { var b = new BrainBuilder(); b.Entry("Patrol"); b.Global().To("Dead").OnEvent(AiSignal.Died); b.State("Patrol") .Tick(c => c.Mover.WalkRandom()) .To("Chase").When(c => c.Sensor.SeesPlayer(), "SeesPlayer"); b.State("Chase") .OnEnter(c => c.Mover.FacePlayer()) .OnExit(c => c.Mover.Stop()) .Tick(c => c.Mover.MoveTo(c.Sensor.LastKnown)) .To("Search").When(c => c.Sensor.LostFor(2f), "LostFor(2s)"); b.State("Search") .To("Patrol").After(3f); b.State("Dead"); return b.Build(); } [Test] public void StartsAtEntry() { var rt = new AiRuntime(Graph(), new FakeAiContext()); Assert.AreEqual("Patrol", rt.CurrentStateName); } [Test] public void Tick_RunsCurrentStateTickAction() { var ctx = new FakeAiContext(); var rt = new AiRuntime(Graph(), ctx); rt.Tick(0.1f); CollectionAssert.Contains(ctx.M.Calls, "WalkRandom"); } [Test] public void ConditionTransition_FiresAndRunsEnterExit() { var ctx = new FakeAiContext(); var rt = new AiRuntime(Graph(), ctx); ctx.S.Sees = true; rt.Tick(0.1f); Assert.AreEqual("Chase", rt.CurrentStateName); CollectionAssert.Contains(ctx.M.Calls, "FacePlayer"); // Chase.OnEnter } [Test] public void Gate_SuspendsDecisionWhenNotControllable() { var ctx = new FakeAiContext(); var rt = new AiRuntime(Graph(), ctx); ctx.S.Sees = true; ctx.V.Controllable = false; // 受击中 rt.Tick(0.1f); Assert.AreEqual("Patrol", rt.CurrentStateName); // 未转换 Assert.IsTrue(rt.IsSuspended); CollectionAssert.DoesNotContain(ctx.M.Calls, "WalkRandom"); // Tick 也不跑 } [Test] public void Gate_ResumesAndReevaluatesWhenControllable() { var ctx = new FakeAiContext(); var rt = new AiRuntime(Graph(), ctx); ctx.S.Sees = true; ctx.V.Controllable = false; rt.Tick(0.1f); ctx.V.Controllable = true; // 恢复 rt.Tick(0.1f); Assert.AreEqual("Chase", rt.CurrentStateName); } [Test] public void After_UsesStateTimer() { var ctx = new FakeAiContext(); var rt = new AiRuntime(Graph(), ctx); ctx.S.Sees = true; rt.Tick(0.1f); // -> Chase ctx.S.Sees = false; ctx.S.LostForValue = 5f; rt.Tick(0.1f); // -> Search Assert.AreEqual("Search", rt.CurrentStateName); rt.Tick(1f); rt.Tick(1f); Assert.AreEqual("Search", rt.CurrentStateName); // 未满 3s rt.Tick(1.5f); Assert.AreEqual("Patrol", rt.CurrentStateName); // 累计 >3s } [Test] public void Switch_RunsPreviousStateOnExit() { var ctx = new FakeAiContext(); var rt = new AiRuntime(Graph(), ctx); ctx.S.Sees = true; rt.Tick(0.1f); // Patrol -> Chase ctx.S.Sees = false; ctx.S.LostForValue = 5f; rt.Tick(0.1f);// Chase -> Search (触发 Chase.OnExit) CollectionAssert.Contains(ctx.M.Calls, "Stop"); } [Test] public void Construct_InvokesEntryOnEnter() { var b = new BrainBuilder(); b.Entry("Start"); b.State("Start").OnEnter(c => c.Mover.LookAround()); var ctx = new FakeAiContext(); var rt = new AiRuntime(b.Build(), ctx); CollectionAssert.Contains(ctx.M.Calls, "LookAround"); } } }