Files
zeling_v2/Assets/Tests/EditMode/GameStateMachineTests.cs

271 lines
10 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using NUnit.Framework;
using BaseGames.Core;
using BaseGames.Core.Events;
namespace BaseGames.Tests.EditMode
{
/// <summary>
/// GameStateMachine + GameStateId 单元测试EditMode
/// 覆盖:状态注册、合法/非法转换、OnEnter/OnExit 回调顺序、Tick 分发。
/// </summary>
[TestFixture]
public class GameStateMachineTests
{
// ── Mock 状态实现 ────────────────────────────────────────────────────
private class MockState : IGameState
{
public GameStateId Id { get; }
public IReadOnlyCollection<GameStateId> 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<GameStateId>(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<GameStateId>(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)");
}
}
}
}