using System.Collections.Generic; using UnityEngine; namespace MazeGen { /// /// The full set of generated rooms so far, plus O(1)-ish id lookup, spatial queries and /// exit-usage bookkeeping. Everything else (agent tracking, visualization, collision/merge /// logic) reads from here instead of walking the scene hierarchy. /// /// Also tracks connected components (union-find over rooms, merged whenever two exits get /// connected) so "does an exit already exist" and "are there still unused exits" can be /// asked per-component instead of globally. That distinction matters once there are /// multiple start rooms: a global answer would let one isolated branch's safety valves get /// silently satisfied by an exit room that lives in a totally different, disconnected part /// of the maze - which is exactly how a branch can end up with no way out at all. /// public class MazeGraph { public readonly Dictionary Rooms = new Dictionary(); public readonly List StartRooms = new List(); public readonly List ExitRooms = new List(); private readonly SpatialRoomIndex _spatialIndex; // Union-find over room ids, keyed by root id. private readonly Dictionary _componentParent = new Dictionary(); private readonly Dictionary _componentHasExit = new Dictionary(); private readonly Dictionary _componentUnusedExitCount = new Dictionary(); private int _nextRoomId; private int _nextExitId; public int RoomCount => Rooms.Count; /// Total exits across the whole graph that are not yet used (informational only - generation decisions should use ComponentUnusedExitCount instead). public int UnusedExitCount { get; private set; } public MazeGraph(float spatialCellSize) { _spatialIndex = new SpatialRoomIndex(spatialCellSize); } public int AllocateRoomId() => _nextRoomId++; public int AllocateExitId() => _nextExitId++; public void Register(Room room) { Rooms[room.Id] = room; if (room.Kind == RoomKind.Start) StartRooms.Add(room); if (room.Kind == RoomKind.Exit) { ExitRooms.Add(room); _componentHasExit[Find(room.Id)] = true; } _spatialIndex.Add(room); } public Room Get(int id) => Rooms.TryGetValue(id, out var room) ? room : null; public IEnumerable QueryRoomsNear(Vector2 position, float radius) => _spatialIndex.QueryNearby(position, radius); /// Adds an exit to a room and keeps the unused-exit counters (global and per-component) in sync. public void AddExit(Room room, Exit exit) { room.Exits.Add(exit); if (!exit.IsUsed) { UnusedExitCount++; _componentUnusedExitCount[Find(room.Id)]++; } } /// Marks an exit as used (idempotent) and keeps the unused-exit counters in sync. public void MarkExitUsed(Exit exit) { if (exit.IsUsed) return; exit.IsUsed = true; UnusedExitCount--; _componentUnusedExitCount[Find(exit.OwnerRoomId)]--; } /// /// Undoes a speculative AddExit for an exit that turned out to be unusable (e.g. a /// door added on-the-fly to close a loop, where the path to it was later found to be /// blocked). Only valid for still-unused exits. /// public void RemoveUnusedExit(Room room, Exit exit) { if (exit.IsUsed) return; if (room.Exits.Remove(exit)) { UnusedExitCount--; _componentUnusedExitCount[Find(room.Id)]--; } } /// Merges the connected components containing the two rooms - call whenever a connection is made between them. public void Union(int roomIdA, int roomIdB) { int a = Find(roomIdA); int b = Find(roomIdB); if (a == b) return; _componentParent[b] = a; _componentHasExit[a] = _componentHasExit[a] || _componentHasExit[b]; _componentUnusedExitCount[a] += _componentUnusedExitCount[b]; _componentHasExit.Remove(b); _componentUnusedExitCount.Remove(b); } public bool AreSameComponent(int roomIdA, int roomIdB) => Find(roomIdA) == Find(roomIdB); /// True if an Exit room already exists anywhere in the same connected component as this room. public bool ComponentHasExit(int roomId) => _componentHasExit[Find(roomId)]; /// Unused exits remaining within this room's connected component only. public int ComponentUnusedExitCount(int roomId) => _componentUnusedExitCount[Find(roomId)]; private int Find(int roomId) { if (!_componentParent.ContainsKey(roomId)) { _componentParent[roomId] = roomId; _componentHasExit[roomId] = false; _componentUnusedExitCount[roomId] = 0; return roomId; } int parent = _componentParent[roomId]; if (parent != roomId) { parent = Find(parent); // path compression _componentParent[roomId] = parent; } return parent; } } }