198 lines
8.0 KiB
C#
198 lines
8.0 KiB
C#
using NUnit.Framework;
|
|
using ProjectM.Simulation;
|
|
using Unity.Collections;
|
|
|
|
namespace ProjectM.Tests
|
|
{
|
|
/// <summary>
|
|
/// Pure-function tests for <see cref="RunMapMath"/> — 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.
|
|
/// </summary>
|
|
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<byte> 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<byte> 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));
|
|
}
|
|
}
|
|
}
|