using Unity.Collections;
using Unity.Mathematics;
namespace ProjectM.Simulation
{
///
/// Pure, deterministic generator for the branching run-map DAG — no RNG state, no wall-clock, INTEGER-HASH ONLY
/// (no , whose draw order is fragile across the multi-pass edge build and
/// which the client would have to replay bit-identically). A run map is therefore a pure function of the run seed,
/// so server + client regenerate the SAME graph — the server keeps gameplay authority (the party's column + the
/// reachable options ride the wire), the client regenerates only to DRAW the map. Mirrors the
/// pure-math discipline.
///
/// Structure: layer 0 = a single Combat landing node; interior layers width 2–3, weighted-typed
/// (Combat 60 / Reward 25 / Elite 15, with an all-Reward-layer guard); the second-to-last layer is an all-Elite
/// gate (so every start→boss path passes ≥1 Elite); the last layer = the single Boss terminal. Edges: a primary
/// pass (every source gets ≥1 proportional out-edge, jittered, sometimes widened) + a coverage pass (every target
/// gets ≥1 in-edge), which together guarantee full reachability from the root and exactly one terminal.
///
public static class RunMapMath
{
// ---- deterministic integer hashing (order-independent combine + a final avalanche) ----
static uint Mix(uint h)
{
h ^= h >> 16; h *= 0x7feb352du;
h ^= h >> 15; h *= 0x846ca68bu;
h ^= h >> 16;
return h;
}
static uint Combine(uint h, uint v)
{
// boost-style hash_combine
h ^= v + 0x9e3779b9u + (h << 6) + (h >> 2);
return h;
}
/// Deterministic hash of a salt tuple (integer-only, well-mixed, never dependent on draw order).
public static uint Hash(uint a) => Mix(Combine(0x811c9dc5u, a));
public static uint Hash(uint a, uint b) => Mix(Combine(Combine(0x811c9dc5u, a), b));
public static uint Hash(uint a, uint b, uint c) => Mix(Combine(Combine(Combine(0x811c9dc5u, a), b), c));
public static uint Hash(uint a, uint b, uint c, uint d) =>
Mix(Combine(Combine(Combine(Combine(0x811c9dc5u, a), b), c), d));
///
/// Generate the branching run map for . Deterministic + identical on both worlds.
///
public static RunMap Generate(uint runSeed)
{
uint s = math.max(1u, runSeed);
int L = 6 + (int)(Hash(s, 0x1Au) % 5u); // run length in [6,10]
var map = new RunMap { LayerCount = (byte)L };
// Per-layer branch widths: single landing + single boss, interior 2–3.
map.LayerWidths = new FixedList64Bytes();
for (int layer = 0; layer < L; layer++)
{
byte w = (layer == 0 || layer == L - 1)
? (byte)1
: (byte)(2 + (int)(Hash(s, (uint)layer, 0x11u) % 2u)); // 2 or 3
map.LayerWidths.Add(w);
}
// Nodes: fixed stride MaxWidth per layer (absent columns left default).
map.Nodes = new FixedList512Bytes();
int slots = L * RunMap.MaxWidth;
for (int i = 0; i < slots; i++) map.Nodes.Add(default);
// Types / biome / shape.
for (int layer = 0; layer < L; layer++)
{
int w = map.LayerWidths[layer];
byte layerBiome = (byte)(Hash(s, (uint)layer, 0xB1u) % RoomBiomeId.Count);
bool anyNonReward = false;
for (int col = 0; col < w; col++)
{
byte type = PickType(s, layer, col, L);
if (type != RoomTypeId.Reward) anyNonReward = true;
byte shape = (byte)(Hash(s, (uint)layer, (uint)col, 0x5Au) % RoomShapeId.Count);
map.Nodes[RunMap.NodeId(layer, col)] = new RunMapNode
{
RoomType = type,
Biome = layerBiome,
ShapeId = shape,
NextMask = 0,
};
}
// Guard: never an entire interior layer of only Reward rooms → force column 0 to Combat.
if (!anyNonReward && w > 0)
{
int id0 = RunMap.NodeId(layer, 0);
var n = map.Nodes[id0];
n.RoomType = RoomTypeId.Combat;
map.Nodes[id0] = n;
}
}
BuildEdges(ref map, s);
return map;
}
static byte PickType(uint s, int layer, int col, int L)
{
if (layer == 0) return RoomTypeId.Combat; // guaranteed landing room
if (layer == L - 1) return RoomTypeId.Boss; // single terminal
if (layer == L - 2) return RoomTypeId.Elite; // all-Elite gate (≥1 Elite on every path)
uint r = Hash(s, (uint)layer, (uint)col, 0xC0u) % 100u; // Combat 60 / Reward 25 / Elite 15
if (r < 60u) return RoomTypeId.Combat;
if (r < 85u) return RoomTypeId.Reward;
return RoomTypeId.Elite;
}
static void BuildEdges(ref RunMap map, uint s)
{
int L = map.LayerCount;
for (int l = 0; l < L - 1; l++)
{
int w = map.LayerWidths[l];
int wn = map.LayerWidths[l + 1];
// Primary: every source gets a proportional out-edge (± jitter), sometimes widened to a neighbor.
for (int c = 0; c < w; c++)
{
int t = ProportionalCol(c, w, wn);
int jitter = (int)(Hash(s, (uint)l, (uint)c, 0xEDu) % 3u) - 1; // -1, 0, +1
t = math.clamp(t + jitter, 0, wn - 1);
SetEdge(ref map, l, c, t);
if (wn > 1 && Hash(s, (uint)l, (uint)c, 0x2Bu) % 100u < 35u)
{
int dir = (Hash(s, (uint)l, (uint)c, 0x2Cu) % 2u) == 0u ? -1 : 1;
int t2 = math.clamp(t + dir, 0, wn - 1);
SetEdge(ref map, l, c, t2);
}
}
// Coverage: every target in the next layer must have ≥1 in-edge (forces convergence on the Boss).
for (int tcol = 0; tcol < wn; tcol++)
{
if (!HasInEdge(ref map, l, tcol))
{
int src = ProportionalCol(tcol, wn, w);
SetEdge(ref map, l, src, tcol);
}
}
}
}
static int ProportionalCol(int from, int fromWidth, int toWidth)
{
if (fromWidth <= 1 || toWidth <= 1) return toWidth / 2;
return (int)math.round((float)from * (toWidth - 1) / (fromWidth - 1));
}
static void SetEdge(ref RunMap map, int layer, int col, int targetCol)
{
int id = RunMap.NodeId(layer, col);
var n = map.Nodes[id];
n.NextMask |= (byte)(1 << targetCol);
map.Nodes[id] = n;
}
static bool HasInEdge(ref RunMap map, int layer, int targetCol)
{
int w = map.LayerWidths[layer];
byte bit = (byte)(1 << targetCol);
for (int c = 0; c < w; c++)
if ((map.Nodes[RunMap.NodeId(layer, c)].NextMask & bit) != 0) return true;
return false;
}
///
/// The columns of the NEXT layer reachable from node (, ).
/// Empty for the Boss/last layer. This is the authoritative set the route-choice offer is drawn from.
///
public static int ReachableOptions(in RunMap map, int layer, int col, out FixedList32Bytes cols)
{
cols = new FixedList32Bytes();
if (layer < 0 || layer >= map.LayerCount - 1) return 0;
byte mask = map.Node(layer, col).NextMask;
int wn = map.Width(layer + 1);
for (int j = 0; j < wn; j++)
if ((mask & (1 << j)) != 0) cols.Add((byte)j);
return cols.Length;
}
///
/// True iff every PRESENT node is reachable from the root (0,0) via the edges (BFS). Used to assert the
/// generator never strands a node or the Boss. O(nodes).
///
public static bool AllNodesReachable(in RunMap map)
{
var visited = new FixedList128Bytes();
for (int i = 0; i < RunMap.MaxNodes; i++) visited.Add(0);
var stack = new FixedList128Bytes();
int root = RunMap.NodeId(0, 0);
visited[root] = 1;
stack.Add((byte)root);
while (stack.Length > 0)
{
int id = stack[stack.Length - 1];
stack.RemoveAt(stack.Length - 1);
int layer = map.LayerOf(id);
if (layer >= map.LayerCount - 1) continue;
byte mask = map.NodeAt(id).NextMask;
int wn = map.Width(layer + 1);
for (int j = 0; j < wn; j++)
{
if ((mask & (1 << j)) == 0) continue;
int nid = RunMap.NodeId(layer + 1, j);
if (visited[nid] == 0) { visited[nid] = 1; stack.Add((byte)nid); }
}
}
for (int layer = 0; layer < map.LayerCount; layer++)
for (int col = 0; col < map.Width(layer); col++)
if (visited[RunMap.NodeId(layer, col)] == 0) return false;
return true;
}
}
}