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; } } }