Files
zeling_v2/Assets/Tests/EditMode/AI/BrainBuilderTests.cs
T
joywayer 80fdc36762 feat(ai): BrainBuilder.DeclareState——重复声明状态即报错,防模块间同名静默覆盖
State() 仍保持宽松(有则取),用于给已声明的态挂边;DeclareState() 只用于
首次声明行为,重复声明同名状态会抛异常。AiStateFragments 的四个建态原语
(Locomotion/Ability/AbilityOnce/Terminal)改用 DeclareState。同时给
IEngagementModule/IDeathModule/IUnawareModule 补充不变量说明。
2026-07-29 12:01:19 +08:00

119 lines
3.7 KiB
C#

using NUnit.Framework;
using BaseGames.AI;
namespace BaseGames.Tests.EditMode.AI
{
public class BrainBuilderTests
{
static AiGraph BuildSample()
{
var b = new BrainBuilder();
b.Entry("Patrol");
b.Global().To("Dead").OnEvent(AiSignal.Died);
b.State("Patrol")
.To("Chase").When(c => c.Sensor.SeesPlayer(), "SeesPlayer");
b.State("Chase")
.To("Patrol").When(c => c.Sensor.LostFor(2f), "LostFor(2s)");
b.State("Dead");
return b.Build();
}
[Test]
public void Build_SetsEntryState()
{
Assert.AreEqual("Patrol", BuildSample().EntryState);
}
[Test]
public void Build_RegistersAllStates()
{
var g = BuildSample();
CollectionAssert.AreEquivalent(
new[] { "Patrol", "Chase", "Dead" },
System.Linq.Enumerable.ToList(g.StateNames));
}
[Test]
public void Build_UsesExplicitConditionLabel()
{
var g = BuildSample();
var t = g.GetState("Patrol").Transitions[0];
Assert.AreEqual("SeesPlayer", t.Label);
}
[Test]
public void Build_UnlabeledCondition_FallsBackToCond()
{
var b = new BrainBuilder();
b.Entry("A");
b.State("A").To("B").When(c => true);
b.State("B");
var g = b.Build();
Assert.AreEqual("cond", g.GetState("A").Transitions[0].Label);
}
[Test]
public void Build_GlobalTransitionsAreSeparate()
{
var g = BuildSample();
Assert.AreEqual(1, g.GlobalTransitions.Count);
Assert.AreEqual("Dead", g.GlobalTransitions[0].Target);
Assert.AreEqual(AiSignal.Died, g.GlobalTransitions[0].Event);
}
[Test]
public void Build_UnknownTransitionTarget_Throws()
{
var b = new BrainBuilder();
b.Entry("A");
b.State("A").To("Nonexistent").When(c => true);
Assert.Throws<System.InvalidOperationException>(() => b.Build());
}
[Test]
public void RequireState_Throws_WhenStateNotDeclared()
{
var b = new BrainBuilder();
var ex = Assert.Throws<System.InvalidOperationException>(() => b.RequireState("Ghost"));
StringAssert.Contains("Ghost", ex.Message);
}
[Test]
public void RequireState_Passes_WhenStateDeclared()
{
var b = new BrainBuilder();
b.State("Real");
Assert.DoesNotThrow(() => b.RequireState("Real"));
}
[Test]
public void DeclareState_Throws_OnDuplicateName()
{
var b = new BrainBuilder();
b.DeclareState("Dup");
var ex = Assert.Throws<System.InvalidOperationException>(() => b.DeclareState("Dup"));
StringAssert.Contains("Dup", ex.Message);
}
[Test]
public void DeclareState_ReturnsUsableBuilder_OnFirstDeclare()
{
var b = new BrainBuilder();
b.Entry("A");
b.DeclareState("A").To("B").When(c => true, "go");
b.DeclareState("B");
Assert.DoesNotThrow(() => b.Build());
}
[Test]
public void State_StaysLenient_ForAttachingToDeclaredState()
{
// State() 仍是"有则取"——骨架要给模块已声明的态挂升级边,靠的就是这个。
var b = new BrainBuilder();
b.Entry("A");
b.DeclareState("A");
Assert.DoesNotThrow(() => b.State("A").To("A").When(c => false, "noop"));
}
}
}