using UnityEngine;
using System.Collections.Generic;
using System.Linq;
///
/// Generates procedural mazes with rooms, hallways, and various terrain types
/// Designed with AI pathfinding in mind - guarantees multiple paths and accessibility
///
public class MazeGenerator
{
public delegate void ProgressUpdate(int completedTiles, int totalTiles, string message);
private MazeConfig config;
private MazeData mazeData;
private System.Random random;
private ProgressUpdate progressCallback;
private int totalTiles;
private int completedTiles;
private string currentPhase;
public MazeGenerator(MazeConfig config, ProgressUpdate progressCallback = null)
{
this.config = config;
this.progressCallback = progressCallback;
config.Validate();
if (config.UseRandomSeed)
{
random = new System.Random();
}
else
{
random = new System.Random(config.RandomSeed);
}
}
private void ReportProgress(string message)
{
var fullMessage = string.IsNullOrEmpty(currentPhase)
? message
: $"{currentPhase}: {message}";
progressCallback?.Invoke(completedTiles, totalTiles, fullMessage);
}
private void SetPhase(string phase)
{
currentPhase = phase;
ReportProgress("Starting");
}
///
/// Generates a complete maze based on the config
///
public MazeData Generate()
{
mazeData = new MazeData(config.Width, config.Height);
totalTiles = config.Width * config.Height;
completedTiles = 0;
SetPhase("Initialize");
// Step 1: Generate room layouts
GenerateRooms();
// Step 2: Connect rooms with hallways
ConnectRooms();
// Step 3: Apply terrain types
ApplyTerrains();
// Step 4: Place start and exit points
PlaceStartAndExitPoints();
// Step 5: Ensure connectivity (verify all rooms are reachable)
EnsureConnectivity();
SetPhase("Complete");
Debug.Log(mazeData.GetStatistics());
return mazeData;
}
///
/// Step 1: Generates room layouts
///
private void GenerateRooms()
{
SetPhase("Generate Rooms");
var rooms = new List();
int attempts = 0;
int maxAttempts = config.TargetRoomCount * 10;
while (rooms.Count < config.TargetRoomCount && attempts < maxAttempts)
{
attempts++;
int width = random.Next(config.MinRoomWidth, config.MaxRoomWidth + 1);
int height = random.Next(config.MinRoomHeight, config.MaxRoomHeight + 1);
int x = random.Next(1, config.Width - width - 1);
int y = random.Next(1, config.Height - height - 1);
var newRoom = new MazeRoom(rooms.Count, x, y, x + width - 1, y + height - 1);
// Check if room overlaps with existing rooms or walls (with spacing)
if (CanPlaceRoom(newRoom, rooms))
{
rooms.Add(newRoom);
}
}
// Assign room types based on configuration
AssignRoomTypes(rooms);
// Fill rooms in maze data
for (int i = 0; i < rooms.Count; i++)
{
var room = rooms[i];
mazeData.AddRoom(room.MinX, room.MinY, room.MaxX, room.MaxY, room.Type);
int filled = mazeData.FillRoom(room.MinX + 1, room.MinY + 1, room.MaxX - 1, room.MaxY - 1, i);
completedTiles += filled;
ReportProgress($"Generated room {i + 1}/{rooms.Count} ({completedTiles}/{totalTiles} tiles)");
}
Debug.Log($"Generated {rooms.Count} rooms");
}
///
/// Checks if a room can be placed without overlapping or being too close to others
///
private bool CanPlaceRoom(MazeRoom newRoom, List existingRooms)
{
// Check bounds
if (newRoom.MinX - config.MinRoomSpacing < 0 || newRoom.MaxX + config.MinRoomSpacing >= config.Width ||
newRoom.MinY - config.MinRoomSpacing < 0 || newRoom.MaxY + config.MinRoomSpacing >= config.Height)
{
return false;
}
// Check overlap/spacing with existing rooms
foreach (var room in existingRooms)
{
// Add spacing buffer
int minX = room.MinX - config.MinRoomSpacing;
int minY = room.MinY - config.MinRoomSpacing;
int maxX = room.MaxX + config.MinRoomSpacing;
int maxY = room.MaxY + config.MinRoomSpacing;
if (!(newRoom.MaxX < minX || newRoom.MinX > maxX ||
newRoom.MaxY < minY || newRoom.MinY > maxY))
{
return false;
}
}
return true;
}
///
/// Assigns room types (safe, rest, boss, etc.) to rooms
///
private void AssignRoomTypes(List rooms)
{
var roomList = new List(rooms);
// Assign special room types
DistributeRoomType(roomList, MazeRoom.RoomType.Safe, config.SafeRoomCount);
DistributeRoomType(roomList, MazeRoom.RoomType.Rest, config.RestRoomCount);
DistributeRoomType(roomList, MazeRoom.RoomType.Boss, config.BossRoomCount);
// Reset unassigned rooms to Normal
foreach (var room in rooms)
{
if (room.Type != MazeRoom.RoomType.Safe &&
room.Type != MazeRoom.RoomType.Rest &&
room.Type != MazeRoom.RoomType.Boss)
{
room.Type = MazeRoom.RoomType.Normal;
}
}
}
///
/// Distributes a specific room type to random rooms
///
private void DistributeRoomType(List rooms, MazeRoom.RoomType type, int count)
{
int assigned = 0;
var available = rooms.Where(r => r.Type == MazeRoom.RoomType.Normal).ToList();
while (assigned < count && available.Count > 0)
{
int idx = random.Next(available.Count);
var room = available[idx];
room.Type = type;
available.RemoveAt(idx);
assigned++;
}
}
///
/// Step 2: Connects rooms with hallways
/// Uses a modified Minimum Spanning Tree approach
///
private void ConnectRooms()
{
SetPhase("Connect Rooms");
var rooms = mazeData.Rooms;
if (rooms.Count == 0) return;
var connected = new HashSet { rooms[0].Id };
var unconnected = new HashSet(rooms.Select(r => r.Id));
unconnected.Remove(rooms[0].Id);
while (unconnected.Count > 0)
{
// Find closest connected-unconnected room pair
float minDist = float.MaxValue;
MazeRoom from = null;
MazeRoom to = null;
foreach (var connectedId in connected)
{
var connectedRoom = rooms.First(r => r.Id == connectedId);
foreach (var unconnectedId in unconnected)
{
var unconnectedRoom = rooms.First(r => r.Id == unconnectedId);
float dist = Vector2Int.Distance(connectedRoom.GetCenter(), unconnectedRoom.GetCenter());
if (dist < minDist)
{
minDist = dist;
from = connectedRoom;
to = unconnectedRoom;
}
}
}
if (from != null && to != null)
{
// Create hallway between rooms
CreateHallway(from, to);
connected.Add(to.Id);
unconnected.Remove(to.Id);
}
}
// Add extra connections for multiple paths
AddExtraConnections(rooms);
Debug.Log("Connected rooms with hallways");
}
///
/// Creates a hallway between two rooms
///
private int CreateHallway(MazeRoom from, MazeRoom to)
{
var pathA = GeneratePath(from.GetCenter(), to.GetCenter());
int changed = mazeData.DrawPath(pathA, config.MinHallwayWidth);
completedTiles += changed;
ReportProgress($"Connected room {from.Id} to {to.Id} ({completedTiles}/{totalTiles} tiles)");
return changed;
}
///
/// Generates a path between two points using A* or simple line algorithm
///
private List GeneratePath(Vector2Int start, Vector2Int end)
{
var path = new List();
// Simple L-shaped path (can be enhanced with A* for better aesthetics)
int x = start.x;
int y = start.y;
// Move horizontally first
while (x != end.x)
{
x += x < end.x ? 1 : -1;
path.Add(new Vector2Int(x, y));
}
// Then vertically
while (y != end.y)
{
y += y < end.y ? 1 : -1;
path.Add(new Vector2Int(x, y));
}
return path;
}
///
/// Adds extra connections between rooms to create multiple paths
///
private void AddExtraConnections(List rooms)
{
// For each room, randomly connect to nearby rooms
int extraConnections = Mathf.Max(1, rooms.Count / 10);
for (int i = 0; i < extraConnections; i++)
{
var room1 = rooms[random.Next(rooms.Count)];
var room2 = rooms[random.Next(rooms.Count)];
if (room1 != room2)
{
CreateHallway(room1, room2);
}
}
}
///
/// Step 3: Applies different terrain types to areas
///
private void ApplyTerrains()
{
SetPhase("Apply Terrains");
var normalRooms = mazeData.GetRoomsByType(MazeRoom.RoomType.Normal);
// Apply swampy terrain to some normal rooms
for (int i = 0; i < normalRooms.Count * 0.2f; i++)
{
var room = normalRooms[random.Next(normalRooms.Count)];
ApplyTerrainToRoom(room, MazeTile.TerrainType.Swamp);
}
// Apply stone terrain to some normal rooms
for (int i = 0; i < normalRooms.Count * 0.1f; i++)
{
var room = normalRooms[random.Next(normalRooms.Count)];
ApplyTerrainToRoom(room, MazeTile.TerrainType.Stone);
}
}
///
/// Applies a specific terrain type to a room
///
private void ApplyTerrainToRoom(MazeRoom room, MazeTile.TerrainType terrain)
{
for (int x = room.MinX + 1; x < room.MaxX; x++)
{
for (int y = room.MinY + 1; y < room.MaxY; y++)
{
var tile = mazeData.GetTile(x, y);
if (tile.Type == MazeTile.TileType.Floor)
{
tile.Terrain = terrain;
}
}
}
}
///
/// Step 4: Places start and exit points
///
private void PlaceStartAndExitPoints()
{
SetPhase("Place Start/Exit Points");
var rooms = mazeData.Rooms;
if (rooms.Count == 0) return;
// Place start points
int startCount = random.Next(config.MinStartPoints, config.MaxStartPoints + 1);
for (int i = 0; i < startCount && i < rooms.Count; i++)
{
var room = rooms[i];
room.IsStart = true;
var point = room.GetRandomPoint();
mazeData.AddStartPoint(point);
}
// Place exit points
int exitCount = random.Next(config.MinExits, config.MaxExits + 1);
var endRooms = mazeData.GetRoomsByType(MazeRoom.RoomType.End);
if (endRooms.Count == 0)
{
// Create end rooms if they don't exist
int exitsPlaced = 0;
int roomIndex = 0;
while (exitsPlaced < exitCount && roomIndex < rooms.Count)
{
if (!rooms[roomIndex].IsStart)
{
rooms[roomIndex].Type = MazeRoom.RoomType.End;
rooms[roomIndex].IsEnd = true;
var point = rooms[roomIndex].GetRandomPoint();
mazeData.AddExitPoint(point);
exitsPlaced++;
}
roomIndex++;
}
}
else
{
foreach (var room in endRooms.Take(exitCount))
{
room.IsEnd = true;
var point = room.GetRandomPoint();
mazeData.AddExitPoint(point);
}
}
Debug.Log($"Placed {startCount} start points and {exitCount} exit points");
}
///
/// Step 5: Ensures all rooms are connected and reachable
///
private void EnsureConnectivity()
{
SetPhase("Ensure Connectivity");
// Simple BFS from first start point to check connectivity
if (mazeData.StartPoints.Count == 0) return;
var start = mazeData.StartPoints[0];
var visited = new HashSet();
var queue = new Queue();
queue.Enqueue(start);
visited.Add(start);
while (queue.Count > 0)
{
var current = queue.Dequeue();
var adjacent = mazeData.GetAdjacentWalkable(current.x, current.y);
foreach (var next in adjacent)
{
if (!visited.Contains(next))
{
visited.Add(next);
queue.Enqueue(next);
}
}
}
Debug.Log($"Connectivity check: {visited.Count} tiles reachable from start point");
}
}