using NUnit.Framework; using ProjectM.Simulation; using Unity.Collections; namespace ProjectM.Tests { /// /// Pure-function tests for — the deterministic branching run-map generator. Pins /// determinism (server + client must regenerate the SAME map), the structural invariants the traversal + route /// choice rely on (run length, single landing, single Boss terminal, an all-Elite gate so every path fights an /// Elite, full reachability, no all-Reward interior layer), and the reachable-options enumeration. No ECS world. /// public class RunMapMathTests { // Sweep a spread of seeds so the structural invariants hold generation-wide, not for one lucky map. static uint[] Seeds() { var s = new uint[64]; for (int i = 0; i < s.Length; i++) s[i] = (uint)(i * 2654435761u + 1u); return s; } [Test] public void Generate_Deterministic_SameSeedSameMap() { foreach (var seed in Seeds()) { var a = RunMapMath.Generate(seed); var b = RunMapMath.Generate(seed); Assert.AreEqual(a.LayerCount, b.LayerCount, $"seed {seed}: LayerCount"); for (int layer = 0; layer < a.LayerCount; layer++) { Assert.AreEqual(a.Width(layer), b.Width(layer), $"seed {seed}: width L{layer}"); for (int col = 0; col < a.Width(layer); col++) Assert.IsTrue(a.Node(layer, col).Equals(b.Node(layer, col)), $"seed {seed}: node ({layer},{col}) differs between regenerations"); } } } [Test] public void Generate_RunLength_InSixToTenInclusive() { foreach (var seed in Seeds()) { int L = RunMapMath.Generate(seed).LayerCount; Assert.GreaterOrEqual(L, 6, $"seed {seed}"); Assert.LessOrEqual(L, RunMap.MaxLayers, $"seed {seed}"); } } [Test] public void Generate_Layer0_IsSingleCombatLanding() { foreach (var seed in Seeds()) { var m = RunMapMath.Generate(seed); Assert.AreEqual(1, m.Width(0), $"seed {seed}: landing width"); Assert.AreEqual(RoomTypeId.Combat, m.Node(0, 0).RoomType, $"seed {seed}: landing type"); } } [Test] public void Generate_LastLayer_IsSingleBossTerminal() { foreach (var seed in Seeds()) { var m = RunMapMath.Generate(seed); int last = m.LayerCount - 1; Assert.AreEqual(1, m.Width(last), $"seed {seed}: boss width"); Assert.AreEqual(RoomTypeId.Boss, m.Node(last, 0).RoomType, $"seed {seed}: boss type"); Assert.AreEqual(0, m.Node(last, 0).NextMask, $"seed {seed}: boss is a terminal (NextMask 0)"); } } [Test] public void Generate_SecondLastLayer_IsAllElite_GuaranteesElitePerPath() { // The only layer feeding the Boss is L-2; every start->boss path traverses it. All-Elite there ⇒ every // path fights >= 1 Elite before the boss. foreach (var seed in Seeds()) { var m = RunMapMath.Generate(seed); int gate = m.LayerCount - 2; for (int col = 0; col < m.Width(gate); col++) Assert.AreEqual(RoomTypeId.Elite, m.Node(gate, col).RoomType, $"seed {seed}: gate node ({gate},{col}) must be Elite"); } } [Test] public void Generate_ExactlyOneTerminal_IsTheBoss() { foreach (var seed in Seeds()) { var m = RunMapMath.Generate(seed); int terminals = 0; for (int layer = 0; layer < m.LayerCount; layer++) for (int col = 0; col < m.Width(layer); col++) if (m.Node(layer, col).NextMask == 0) terminals++; Assert.AreEqual(1, terminals, $"seed {seed}: exactly one terminal node (the boss)"); } } [Test] public void Generate_EveryNonBossNode_HasAnOutEdge() { foreach (var seed in Seeds()) { var m = RunMapMath.Generate(seed); for (int layer = 0; layer < m.LayerCount - 1; layer++) for (int col = 0; col < m.Width(layer); col++) Assert.AreNotEqual(0, m.Node(layer, col).NextMask, $"seed {seed}: node ({layer},{col}) has no out-edge"); } } [Test] public void Generate_AllNodesReachableFromRoot() { foreach (var seed in Seeds()) Assert.IsTrue(RunMapMath.AllNodesReachable(RunMapMath.Generate(seed)), $"seed {seed}: a node was stranded (unreachable from the root)"); } [Test] public void Generate_InteriorLayerWidths_AreTwoOrThree() { foreach (var seed in Seeds()) { var m = RunMapMath.Generate(seed); for (int layer = 1; layer < m.LayerCount - 1; layer++) { int w = m.Width(layer); Assert.IsTrue(w == 2 || w == 3, $"seed {seed}: interior layer {layer} width {w} not in {{2,3}}"); } } } [Test] public void Generate_NoInteriorLayerIsAllReward() { foreach (var seed in Seeds()) { var m = RunMapMath.Generate(seed); for (int layer = 1; layer < m.LayerCount - 1; layer++) { bool anyNonReward = false; for (int col = 0; col < m.Width(layer); col++) if (m.Node(layer, col).RoomType != RoomTypeId.Reward) anyNonReward = true; Assert.IsTrue(anyNonReward, $"seed {seed}: interior layer {layer} is entirely Reward"); } } } [Test] public void ReachableOptions_MatchNextMask_AndStayInNextWidth() { foreach (var seed in Seeds()) { var m = RunMapMath.Generate(seed); for (int layer = 0; layer < m.LayerCount - 1; layer++) for (int col = 0; col < m.Width(layer); col++) { int n = RunMapMath.ReachableOptions(m, layer, col, out FixedList32Bytes cols); Assert.Greater(n, 0, $"seed {seed}: node ({layer},{col}) offered no options"); Assert.AreEqual(n, cols.Length); byte mask = m.Node(layer, col).NextMask; int wn = m.Width(layer + 1); for (int i = 0; i < cols.Length; i++) { Assert.Less(cols[i], (byte)wn, $"seed {seed}: option out of next-layer width"); Assert.AreNotEqual(0, mask & (1 << cols[i]), $"seed {seed}: option not set in NextMask"); } } } } [Test] public void ReachableOptions_BossLayer_ReturnsNone() { var m = RunMapMath.Generate(12345u); int n = RunMapMath.ReachableOptions(m, m.LayerCount - 1, 0, out FixedList32Bytes cols); Assert.AreEqual(0, n); Assert.AreEqual(0, cols.Length); } [Test] public void Hash_IsDeterministic_AndSensitiveToInputs() { Assert.AreEqual(RunMapMath.Hash(7u, 3u), RunMapMath.Hash(7u, 3u)); Assert.AreEqual(RunMapMath.Hash(1u, 2u, 3u), RunMapMath.Hash(1u, 2u, 3u)); Assert.AreNotEqual(RunMapMath.Hash(7u, 3u), RunMapMath.Hash(3u, 7u), "order-sensitive"); Assert.AreNotEqual(RunMapMath.Hash(1u), RunMapMath.Hash(2u)); } } }