using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Text; using Unity.VisualScripting; using Random = System.Random; namespace DungeonGenerator { public class DungeonGenerator { private const int SIDE_LENGTH_OF_MONSTER = 4; private const int SIDE_LENGTH_OF_NORMAL = 2; private const int SIDE_LENGTH_OF_ENTRANCE = 2; private const int WIDTH_OF_BOSS = 10; private const int HEIGHT_OF_BOSS = 6; private int _xLength = 40; private int _yLength = 28; public DungeonMap GenerateDungeon(int length, float monsterRoomRatio) { _xLength = 40; _yLength = 28; Random random = new Random(); DungeonMap dungeonMap = new DungeonMap(_xLength, _yLength); dungeonMap.AddRooms(GenerateEntranceRooms(_xLength, _yLength, random.Next(1,4))); dungeonMap.AddRoom(GenerateOnlyBossRoom(_xLength, _yLength, WIDTH_OF_BOSS, HEIGHT_OF_BOSS)); EvenDisperser disperser = new EvenDisperser(_xLength, _yLength, dungeonMap.GetUnoccupiedPoints()); //TODO calculate L and W from length int numberOfMonsterRooms = 7; // TODO: Calculate from ratio for (var i = 0; i < numberOfMonsterRooms; i ++) { dungeonMap.AddRoom(disperser.GenerateAndPlaceRoom( SIDE_LENGTH_OF_MONSTER, SIDE_LENGTH_OF_MONSTER, RoomType.Monster)); } AddNormalRoomsAroundMonsterRooms(dungeonMap); AddNormalRoomsAroundBossRoom(dungeonMap); AddConnectionRooms(dungeonMap); return dungeonMap; } private void AddConnectionRooms(DungeonMap dungeon) { // For each room adjacent to a monster and boos room connect it with the closest thing (entrance room, or monster adjacent room) // unless there is something already going there. List rootRooms = new List(dungeon.GetMonsterRooms()); rootRooms.Add(dungeon.GetBossRoom()); // Create list of connection rooms Dictionary nodeRoomAndSideItIsOn = new Dictionary(); List nodeRoomsWithEntrances = new List(dungeon.GetNodeRooms()); nodeRoomsWithEntrances.AddRange(dungeon.GetEntranceRooms()); // For each connection room that is not an entrance room, // find the next closest uninterrupted connection room that is not on the same root room. foreach (var roomSideWithRoom in nodeRoomAndSideItIsOn) { Room closestSeenRoom = FindClosestSeenConnection(roomSideWithRoom.Value, roomSideWithRoom.Key, nodeRoomsWithEntrances, rootRooms); //Place rooms along the line until they are connected dungeon.AddRooms(PlaceRoomsOrganicallyTowardRoom(roomSideWithRoom.Value, closestSeenRoom, RoomType.Normal, dungeon.GetOccupiedPoints())); } } private void AddNormalRoomsAroundBossRoom(DungeonMap dungeon) { AddNormalRoomsAroundRoom(dungeon.GetBossRoom(), dungeon); } private void AddNormalRoomsAroundMonsterRooms(DungeonMap dungeon) { foreach (Room monsterRoom in dungeon.GetMonsterRooms()) { AddNormalRoomsAroundRoom(monsterRoom, dungeon); } } private void AddNormalRoomsAroundRoom(Room room, DungeonMap dungeon) { Random random = new Random(); List topUnoccupiedPoints = new List(GetUnoccupiedPointsOnSide(room, RoomSide.Top, dungeon.GetUnoccupiedPoints())); List bottomUnoccupiedPoints = new List(GetUnoccupiedPointsOnSide(room, RoomSide.Bottom, dungeon.GetUnoccupiedPoints())); List leftUnoccupiedPoints = new List(GetUnoccupiedPointsOnSide(room, RoomSide.Left, dungeon.GetUnoccupiedPoints())); List rightUnoccupiedPoints = new List(GetUnoccupiedPointsOnSide(room, RoomSide.Right, dungeon.GetUnoccupiedPoints())); int minNecessaryAvailablePoints = (SIDE_LENGTH_OF_MONSTER + SIDE_LENGTH_OF_NORMAL) / 2; List availableSides = new List(); if (topUnoccupiedPoints.Count >= minNecessaryAvailablePoints){availableSides.Add(RoomSide.Top);} if (bottomUnoccupiedPoints.Count >= minNecessaryAvailablePoints){availableSides.Add(RoomSide.Bottom);} if (leftUnoccupiedPoints.Count >= minNecessaryAvailablePoints){availableSides.Add(RoomSide.Left);} if (rightUnoccupiedPoints.Count >= minNecessaryAvailablePoints){availableSides.Add(RoomSide.Right);} // Ensure between 3 and 4 rooms are added int numRoomsToAdd = Math.Min(availableSides.Count, random.Next(3, 5)); // Randomly shuffle and take the required number of sides foreach (RoomSide side in availableSides.OrderBy(_ => random.Next()).Take(numRoomsToAdd)) { List unoccupiedPointsOnSide = side switch { RoomSide.Top => topUnoccupiedPoints, RoomSide.Bottom => bottomUnoccupiedPoints, RoomSide.Left => leftUnoccupiedPoints, RoomSide.Right => rightUnoccupiedPoints, _ => throw new ArgumentOutOfRangeException(nameof(side), $"Unexpected RoomSide value: {side}") }; // Create room and add it if valid Room newRoom = CreateAdjacentRoom(RoomType.Normal, unoccupiedPointsOnSide, side, dungeon.GetOccupiedPoints()); if (newRoom != null) { room.AddAdjacentRoom(newRoom, side); dungeon.AddRoom(newRoom); } } } private List GetUnoccupiedPointsOnSide(Room room, RoomSide side, HashSet unoccupiedPoints) { List unoccupiedPointsOnSide = new List(); var roomPoints = room.GetPointsInRoom(); int minX = roomPoints.Min(p => p.X); int maxX = roomPoints.Max(p => p.X); int minY = roomPoints.Min(p => p.Y); int maxY = roomPoints.Max(p => p.Y); switch (side) { case RoomSide.Top: for (int x = minX - SIDE_LENGTH_OF_NORMAL; x <= maxX + SIDE_LENGTH_OF_NORMAL; x++) { Point point = new Point(x, minY - SIDE_LENGTH_OF_NORMAL); if (unoccupiedPoints.Contains(point)) { unoccupiedPointsOnSide.Add(point); } } return unoccupiedPointsOnSide; case RoomSide.Bottom: for (int x = minX - SIDE_LENGTH_OF_NORMAL; x <= maxX + SIDE_LENGTH_OF_NORMAL; x++) { Point point = new Point(x, maxY + SIDE_LENGTH_OF_NORMAL); if (unoccupiedPoints.Contains(point)) { unoccupiedPointsOnSide.Add(point); } } return unoccupiedPointsOnSide; case RoomSide.Left: for (int y = minY - SIDE_LENGTH_OF_NORMAL; y <= maxY + SIDE_LENGTH_OF_NORMAL; y++) { Point point = new Point(minX - SIDE_LENGTH_OF_NORMAL, y); if (unoccupiedPoints.Contains(point)) { unoccupiedPointsOnSide.Add(point); } } return unoccupiedPointsOnSide; case RoomSide.Right: for (int y = minY - SIDE_LENGTH_OF_NORMAL; y <= maxY + SIDE_LENGTH_OF_NORMAL; y++) { Point point = new Point(maxX + SIDE_LENGTH_OF_NORMAL, y); if (unoccupiedPoints.Contains(point)) { unoccupiedPointsOnSide.Add(point); } } return unoccupiedPointsOnSide; default: throw new ArgumentException("Side not recognized"); } } private Room CreateAdjacentRoom(RoomType type, List unoccupiedPointsOnSide, RoomSide side, HashSet occupiedPoints) { int sizeOfNewRoom = GetRoomSizeByType(type); Random random = new Random(); // Sort points by their coordinate based on the side of placement var orderedPoints = side == RoomSide.Left || side == RoomSide.Right ? unoccupiedPointsOnSide.OrderBy(p => p.Y).ToList() // Sort by Y for vertical placement : unoccupiedPointsOnSide.OrderBy(p => p.X).ToList(); // List to store possible valid room placements (top-left points of the room) List validPlacements = new List(); // Iterate over the ordered points to find potential placements for (int i = 0; i < orderedPoints.Count - sizeOfNewRoom + 1; i++) { // Get a sequence of SIZE_OF_ROOM points starting from index i var potentialPoints = orderedPoints.Skip(i).Take(sizeOfNewRoom).ToList(); // Check if all points are consecutive and fit within the range if (IsRoomWideEnough(potentialPoints, side)) { Point firstPoint = potentialPoints.First(); // Adjust the first point for different sides Point adjustedPoint = side switch { RoomSide.Top => new Point(firstPoint.X, firstPoint.Y), // Move up for top side RoomSide.Bottom => new Point(firstPoint.X, firstPoint.Y - 1), // Move down for bottom side RoomSide.Left => new Point(firstPoint.X, firstPoint.Y), // Move left for left side RoomSide.Right => new Point(firstPoint.X - 1, firstPoint.Y), // Move right for right side _ => firstPoint // Default case (shouldn't happen) }; if (!potentialPoints.Any(occupiedPoints.Contains)) { validPlacements.Add(adjustedPoint); // First point is the top-left point of the room } } } // If there are valid placements, select a random one and create the room if (validPlacements.Any()) { Point randomPlacement = validPlacements[random.Next(validPlacements.Count)]; Room newRoom = new Room(type, sizeOfNewRoom, sizeOfNewRoom, randomPlacement); return newRoom; } // If no valid placements are found, return null or handle it as necessary return null; } // Helper method to check if a sequence of points is wide enough (consecutive points) private bool IsRoomWideEnough(List points, RoomSide side) { for (int i = 1; i < points.Count; i++) { if (side == RoomSide.Left || side == RoomSide.Right) { // Check if points are consecutive along the Y-axis for left/right side if (points[i].Y != points[i - 1].Y + 1) { return false; } } else { // Check if points are consecutive along the X-axis for top/bottom side if (points[i].X != points[i - 1].X + 1) { return false; } } } return true; } private int GetRoomSizeByType(RoomType type) { switch (type) { case RoomType.Entrance: return SIDE_LENGTH_OF_ENTRANCE; case RoomType.Monster: return SIDE_LENGTH_OF_MONSTER; case RoomType.Normal: return SIDE_LENGTH_OF_NORMAL; default: return SIDE_LENGTH_OF_NORMAL; } } private Room GenerateOnlyBossRoom(int xLengthOfDungeon, int yLengthOfDungeon, int width, int heigth) { // Place monster room in the middle third int middleAreaX = xLengthOfDungeon / 3; int middleAreaY = yLengthOfDungeon / 3; Random random = new Random(); int bossX = middleAreaX + random.Next(0, middleAreaX - width); int bossY = middleAreaY + random.Next(0, middleAreaY - heigth); return new Room(RoomType.Boss, width, heigth, new Point(bossX, bossY)); } private enum Side { Top, Right, Bottom, Left } private List GenerateEntranceRooms(int xLengthOfDungeon, int yLengthOfDungeon, int numberOfEntranceLines) { if (numberOfEntranceLines > 4) { throw new Exception("Number of entrance lines cannot be greater than 4"); } const int numEntranceRooms = 12; List entranceRooms = new List(); Random random = new Random(); Dictionary sidesToEnterOn = new() { { Side.Top, 0}, { Side.Right, 0}, { Side.Bottom, 0}, { Side.Left, 0} }; // Randomly assign starting lines to sides of dungeon for (var i = 0; i < numberOfEntranceLines; i++) { while (true) { Side side = (Side)random.Next(0, 4); if (sidesToEnterOn[side] < 2) { sidesToEnterOn[side] += 1; break; } } } int roomsPerLine = numEntranceRooms / numberOfEntranceLines; int bottom = yLengthOfDungeon - SIDE_LENGTH_OF_NORMAL; int oneRoomFromBottom = bottom - SIDE_LENGTH_OF_NORMAL; int right = xLengthOfDungeon - SIDE_LENGTH_OF_NORMAL; // Generate entrance lines on each side switch (sidesToEnterOn[Side.Top]) { case 2: // Add half of points for this entrance line to the top left side for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL) { entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(i,0))); } // Add the rest of the points for this entrance line to the top right side. for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL) { entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(right - i,0))); } break; case 1: int startOfLine = GetStartOfCenteredLine(xLengthOfDungeon, roomsPerLine, SIDE_LENGTH_OF_NORMAL); for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL) { entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL,new Point(startOfLine + i, 0))); } break; } switch (sidesToEnterOn[Side.Right]) { case 2: // Add points for this entrance line to the top right side for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL) { entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(right, i + SIDE_LENGTH_OF_NORMAL))); } // Add the rest of the points for this entrance line to the bottom right side. for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL) { entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(right, oneRoomFromBottom - i))); } break; case 1: int startOfLine = GetStartOfCenteredLine(yLengthOfDungeon, roomsPerLine, SIDE_LENGTH_OF_NORMAL); for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL) { entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(right, startOfLine + i))); } break; } switch (sidesToEnterOn[Side.Bottom]) { case 2: // Add half of points for this entrance line to the bottom left side for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL) { entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(i, bottom))); } // Add the rest of the points for this entrance line to the bottom right side. for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL) { entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point((xLengthOfDungeon - 1) - i, bottom))); } break; case 1: int startOfLine = GetStartOfCenteredLine(xLengthOfDungeon, roomsPerLine, SIDE_LENGTH_OF_NORMAL); for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL) { entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(startOfLine + i, bottom))); } break; } switch (sidesToEnterOn[Side.Left]) { case 2: // Add half of points for this entrance line to the top left side for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL) { entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(0, i + SIDE_LENGTH_OF_NORMAL))); } // Add the rest of the points for this entrance line to the bottom left side. for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL) { entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(0, oneRoomFromBottom - i))); } break; case 1: int startOfLine = GetStartOfCenteredLine(yLengthOfDungeon, roomsPerLine, SIDE_LENGTH_OF_NORMAL); for (var i = 0; i < roomsPerLine * SIDE_LENGTH_OF_NORMAL; i += SIDE_LENGTH_OF_NORMAL) { entranceRooms.Add(new Room(RoomType.Entrance, SIDE_LENGTH_OF_NORMAL, SIDE_LENGTH_OF_NORMAL, new Point(0, startOfLine + i))); } break; } return entranceRooms; } private int GetStartOfCenteredLine(int length, int numberOfRooms, int sizeOfRoom) { int midpoint = length / 2; return midpoint - (numberOfRooms * sizeOfRoom / 2); } private Room FindClosestSeenConnection(Room nodeRoom, RoomSide attachedSide, List possibleConnections, List obstructions) { const float minClosestLength = 0; Vector2 lookDirection = attachedSide switch { RoomSide.Top => new Vector2(0, -1), // Expanding upward RoomSide.Bottom => new Vector2(0, 1), // Expanding downward RoomSide.Left => new Vector2(-1, 0), // Expanding left RoomSide.Right => new Vector2(1, 0), // Expanding right _ => Vector2.Zero // Default (should not happen) }; Point nodeCenter = nodeRoom.GetCenterOfRoom(); float visionConeAngle = MathF.PI / 4; // 45 degree cone in each side Room closestSeenRoom = null; float closestDistance = float.MaxValue; foreach (Room otherRoom in possibleConnections) { if (otherRoom == nodeRoom) continue; Point otherCenter = otherRoom.GetCenterOfRoom(); Vector2 toOther = new Vector2(otherCenter.X - nodeCenter.X, otherCenter.Y - nodeCenter.Y); float distance = toOther.Length(); if (distance <= minClosestLength || distance >= closestDistance) continue; // Normalize direction vectors toOther = Vector2.Normalize(toOther); Vector2 lookNormalized = Vector2.Normalize(lookDirection); float angle = MathF.Acos(Vector2.Dot(lookNormalized, toOther)); if (angle <= visionConeAngle) { if (!IsObstructed(nodeCenter, otherCenter, obstructions)) { closestSeenRoom = otherRoom; closestDistance = distance; } } } return closestSeenRoom; // Could be null if no valid room is found } private bool IsObstructed(Point start, Point end, List obstructions) { List linePoints = GetPointsOnLine(start, end); foreach (Point point in linePoints) { if (obstructions.Any(room => room.ContainsPoint(point))) { return true; } } return false; } // Bresenham’s Line Algorithm to get points between two points private List GetPointsOnLine(Point p0, Point p1) { List points = new List(); int dx = Math.Abs(p1.X - p0.X), sx = p0.X < p1.X ? 1 : -1; int dy = -Math.Abs(p1.Y - p0.Y), sy = p0.Y < p1.Y ? 1 : -1; int err = dx + dy, e2; int x = p0.X, y = p0.Y; while (true) { points.Add(new Point(x, y)); if (x == p1.X && y == p1.Y) break; e2 = 2 * err; if (e2 >= dy) { err += dy; x += sx; } if (e2 <= dx) { err += dx; y += sy; } } return points; } private List PlaceRoomsOrganicallyTowardRoom(Room startingRoom, Room targetRoom, RoomType type, HashSet occupiedPoints) { List placedRooms = new List(); Room currentRoom = startingRoom; bool sideToggle = true; // Start with left placement first List path = GetPointsOnLine(startingRoom.GetCenterOfRoom(), targetRoom.GetCenterOfRoom()); foreach (Point p in path) { List availableSides = sideToggle ? new List { RoomSide.Left, RoomSide.Top } : new List { RoomSide.Right, RoomSide.Bottom }; sideToggle = !sideToggle; // Alternate side for next placement. foreach (RoomSide side in availableSides) { List unoccupiedPointsOnSide = GetUnoccupiedPointsOnSide(currentRoom, side, occupiedPoints); Room newRoom = CreateAdjacentRoom(RoomType.Normal, unoccupiedPointsOnSide, side, occupiedPoints); if (newRoom != null) { placedRooms.Add(newRoom); occupiedPoints.AddRange(newRoom.GetPointsInRoom()); currentRoom = newRoom; break; // Move to the next room } } if (currentRoom.GetCenterOfRoom().ManhattanDistance(targetRoom.GetCenterOfRoom()) <= GetRoomSizeByType(type)) break; } return placedRooms; } } // Class } // Namespace