using System.Collections.Generic; using NUnit.Framework; using BaseGames.Core; using BaseGames.Core.Events; namespace BaseGames.Tests.EditMode { /// /// GameStateMachine + GameStateId 单元测试(EditMode)。 /// 覆盖:状态注册、合法/非法转换、OnEnter/OnExit 回调顺序、Tick 分发。 /// [TestFixture] public class GameStateMachineTests { // ── Mock 状态实现 ──────────────────────────────────────────────────── private class MockState : IGameState { public GameStateId Id { get; } public IReadOnlyCollection ValidNextStates { get; } public bool OnEnterCalled { get; private set; } public bool OnExitCalled { get; private set; } public GameStateId EnteredFrom { get; private set; } public GameStateId ExitedTo { get; private set; } public int TickCount { get; private set; } public MockState(GameStateId id, params GameStateId[] validNext) { Id = id; ValidNextStates = new HashSet(validNext); } public void OnEnter(GameStateId from) { OnEnterCalled = true; EnteredFrom = from; } public void OnExit(GameStateId to) { OnExitCalled = true; ExitedTo = to; } public void Tick(float dt) => TickCount++; } private static readonly GameStateId StateA = new GameStateId("StateA"); private static readonly GameStateId StateB = new GameStateId("StateB"); private static readonly GameStateId StateC = new GameStateId("StateC"); // ── 初始状态 ───────────────────────────────────────────────────────── [Test] public void GameStateMachine_InitialState_IsDefault() { var fsm = new GameStateMachine(); Assert.AreEqual(default(GameStateId), fsm.CurrentStateId, "未注册任何状态时,CurrentStateId 应为 default"); } // ── 状态注册与转换 ──────────────────────────────────────────────────── [Test] public void TransitionTo_UnknownState_ReturnsFalse() { var fsm = new GameStateMachine(); bool result = fsm.TransitionTo(StateA, out string error); Assert.IsFalse(result, "未注册的状态转换应返回 false"); StringAssert.Contains("未知状态", error); } [Test] public void TransitionTo_RegisteredState_FromNull_Succeeds() { var fsm = new GameStateMachine(); var stateA = new MockState(StateA); fsm.Register(stateA); bool result = fsm.TransitionTo(StateA, out string error); Assert.IsTrue(result, "注册状态后首次转换应成功"); Assert.IsNull(error, "成功转换时 error 应为 null"); Assert.AreEqual(StateA, fsm.CurrentStateId); } [Test] public void TransitionTo_ValidNextState_Succeeds() { var fsm = new GameStateMachine(); var stateA = new MockState(StateA, StateB); // A 可以转换到 B var stateB = new MockState(StateB); fsm.Register(stateA); fsm.Register(stateB); fsm.TransitionTo(StateA, out _); bool result = fsm.TransitionTo(StateB, out string error); Assert.IsTrue(result); Assert.IsNull(error); Assert.AreEqual(StateB, fsm.CurrentStateId); } [Test] public void TransitionTo_InvalidNextState_ReturnsFalse() { var fsm = new GameStateMachine(); var stateA = new MockState(StateA); // A 无有效下一状态 var stateB = new MockState(StateB); fsm.Register(stateA); fsm.Register(stateB); fsm.TransitionTo(StateA, out _); bool result = fsm.TransitionTo(StateB, out string error); Assert.IsFalse(result, "非法转换应返回 false"); StringAssert.Contains("非法转换", error); Assert.AreEqual(StateA, fsm.CurrentStateId, "非法转换后状态不应改变"); } // ── OnEnter / OnExit 回调 ──────────────────────────────────────────── [Test] public void TransitionTo_CallsOnExit_OnPreviousState() { var fsm = new GameStateMachine(); var stateA = new MockState(StateA, StateB); var stateB = new MockState(StateB); fsm.Register(stateA); fsm.Register(stateB); fsm.TransitionTo(StateA, out _); fsm.TransitionTo(StateB, out _); Assert.IsTrue(stateA.OnExitCalled, "转出 StateA 时应调用 OnExit"); Assert.AreEqual(StateB, stateA.ExitedTo, "OnExit 传入的下一状态应为 StateB"); } [Test] public void TransitionTo_CallsOnEnter_OnNextState() { var fsm = new GameStateMachine(); var stateA = new MockState(StateA, StateB); var stateB = new MockState(StateB); fsm.Register(stateA); fsm.Register(stateB); fsm.TransitionTo(StateA, out _); fsm.TransitionTo(StateB, out _); Assert.IsTrue(stateB.OnEnterCalled, "转入 StateB 时应调用 OnEnter"); Assert.AreEqual(StateA, stateB.EnteredFrom, "OnEnter 传入的前一状态应为 StateA"); } [Test] public void TransitionTo_FirstEntry_OnEnterFromIsDefault() { var fsm = new GameStateMachine(); var stateA = new MockState(StateA); fsm.Register(stateA); fsm.TransitionTo(StateA, out _); Assert.AreEqual(default(GameStateId), stateA.EnteredFrom, "首次进入状态时,OnEnter 的 from 参数应为 default"); } // ── Tick ────────────────────────────────────────────────────────────── [Test] public void Tick_DelegatesTo_CurrentState() { var fsm = new GameStateMachine(); var stateA = new MockState(StateA); fsm.Register(stateA); fsm.TransitionTo(StateA, out _); fsm.Tick(0.016f); fsm.Tick(0.016f); Assert.AreEqual(2, stateA.TickCount, "Tick 应转发给当前状态"); } [Test] public void Tick_NoCurrentState_DoesNotThrow() { var fsm = new GameStateMachine(); Assert.DoesNotThrow(() => fsm.Tick(0.016f)); } // ── 重复注册 ───────────────────────────────────────────────────────── [Test] public void Register_SameId_OverwritesPreviousState() { var fsm = new GameStateMachine(); var stateA1 = new MockState(StateA); var stateA2 = new MockState(StateA, StateB); // 重新注册,含 B 作为有效下一状态 fsm.Register(stateA1); fsm.Register(stateA2); fsm.TransitionTo(StateA, out _); var stateB = new MockState(StateB); fsm.Register(stateB); bool result = fsm.TransitionTo(StateB, out _); Assert.IsTrue(result, "重新注册后的状态(含 ValidNextStates)应覆盖旧注册"); } // ── GameStateId 值相等性 ─────────────────────────────────────────────── [Test] public void GameStateId_SameKey_AreEqual() { var id1 = new GameStateId("TestState"); var id2 = new GameStateId("TestState"); Assert.AreEqual(id1, id2, "相同 key 的 GameStateId 应相等"); } [Test] public void GameStateId_DifferentKey_AreNotEqual() { var id1 = new GameStateId("StateA"); var id2 = new GameStateId("StateB"); Assert.AreNotEqual(id1, id2); } [Test] public void GameStateId_DefaultIsNotEqualToNamed() { var named = new GameStateId("Something"); var defaultId = default(GameStateId); Assert.AreNotEqual(named, defaultId); } // ── BuiltinGameStates 唯一性 ─────────────────────────────────────────── [Test] public void BuiltinStates_AllIdsAreUnique() { var ids = new[] { GameStates.Initializing, GameStates.MainMenu, GameStates.Gameplay, GameStates.Paused, GameStates.BossFight, GameStates.Cutscene, GameStates.LoadingScene, GameStates.Dead, GameStates.GameOver, }; var set = new HashSet(ids); Assert.AreEqual(ids.Length, set.Count, "9 个内置游戏状态 ID 必须全部唯一"); } [Test] public void BuiltinStates_NoneIsDefault() { var ids = new[] { GameStates.Initializing, GameStates.MainMenu, GameStates.Gameplay, GameStates.Paused, GameStates.BossFight, GameStates.Cutscene, GameStates.LoadingScene, GameStates.Dead, GameStates.GameOver, }; foreach (var id in ids) { Assert.AreNotEqual(default(GameStateId), id, $"内置状态 {id} 不应等于 default(GameStateId)"); } } } }