PuzzleGame/PuzzleGameProject/Assets/Scripts/DungeonGenerator/DungeonGenerator.cs

398 lines
No EOL
18 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Unity.VisualScripting;
using UnityEngine;
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);
return dungeonMap;
}
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();
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);
List<Point> topUnoccupiedPoints = new List<Point>();
List<Point> bottomUnoccupiedPoints = new List<Point>();
List<Point> leftUnoccupiedPoints = new List<Point>();
List<Point> rightUnoccupiedPoints = new List<Point>();
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 (dungeon.GetUnoccupiedPoints().Contains(point))
{
topUnoccupiedPoints.Add(point);
}
point = new Point(x, maxY + SIDE_LENGTH_OF_NORMAL);
if (dungeon.GetUnoccupiedPoints().Contains(point))
{
bottomUnoccupiedPoints.Add(point);
}
}
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 (dungeon.GetUnoccupiedPoints().Contains(point))
{
leftUnoccupiedPoints.Add(point);
}
point = new Point(maxX + SIDE_LENGTH_OF_NORMAL, y);
if (dungeon.GetUnoccupiedPoints().Contains(point))
{
rightUnoccupiedPoints.Add(point);
}
}
int minNecessaryAvailablePoints = (SIDE_LENGTH_OF_MONSTER + SIDE_LENGTH_OF_NORMAL) / 2;
List<RoomSide> availableSides = new List<RoomSide>();
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))
{
// Select appropriate unoccupied points based on the side
List<Point> unoccupiedPointsOnSide = side switch
{
RoomSide.Top => topUnoccupiedPoints,
RoomSide.Bottom => bottomUnoccupiedPoints,
RoomSide.Left => leftUnoccupiedPoints,
RoomSide.Right => rightUnoccupiedPoints,
_ => new List<Point>()
};
// 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 Room CreateAdjacentRoom(RoomType type, List<Point> unoccupiedPointsOnSide, RoomSide side, HashSet<Point> 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<Point> validPlacements = new List<Point>();
// 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<Point> 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<Room> 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<Room> entranceRooms = new List<Room>();
Random random = new Random();
Dictionary<Side, int> 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);
}
}
}