Moved Dungeon Map Generator to it's own solution.

This commit is contained in:
Max 2025-02-10 15:43:13 +01:00
parent 5c46281334
commit fd2a28afe5
50 changed files with 86389 additions and 84 deletions

View file

@ -0,0 +1,13 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/contentModel.xml
/.idea.DungeonMapGenerator.iml
/projectSettingsUpdater.xml
/modules.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

View file

@ -0,0 +1,20 @@
// See https://aka.ms/new-console-template for more information
using DungeonMapGenerator;
class Program
{
static void Main(string[] args)
{
// Create an instance of your DungeonMapGenerator class
var generator = new DungeonGenerator();
generator.GenerateDungeon(25, .5f);
// Call the method you want to run
var map = generator.GenerateDungeon(25, .5f);
// Print the map to the console (assuming it returns a string or something printable)
Console.WriteLine(map.GetMapAsString());
Console.ReadLine();
}
}

View file

@ -0,0 +1,66 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v9.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v9.0": {
"DungeonMapConsolePrinter/1.0.0": {
"dependencies": {
"DungeonMapGenerator": "1.0.0"
},
"runtime": {
"DungeonMapConsolePrinter.dll": {}
}
},
"Newtonsoft.Json/13.0.3": {
"runtime": {
"lib/net6.0/Newtonsoft.Json.dll": {
"assemblyVersion": "13.0.0.0",
"fileVersion": "13.0.3.27908"
}
}
},
"System.Numerics.Vectors/4.6.0": {},
"DungeonMapGenerator/1.0.0": {
"dependencies": {
"Newtonsoft.Json": "13.0.3",
"System.Numerics.Vectors": "4.6.0"
},
"runtime": {
"DungeonMapGenerator.dll": {
"assemblyVersion": "1.0.0",
"fileVersion": "1.0.0.0"
}
}
}
}
},
"libraries": {
"DungeonMapConsolePrinter/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Newtonsoft.Json/13.0.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==",
"path": "newtonsoft.json/13.0.3",
"hashPath": "newtonsoft.json.13.0.3.nupkg.sha512"
},
"System.Numerics.Vectors/4.6.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-t+SoieZsRuEyiw/J+qXUbolyO219tKQQI0+2/YI+Qv7YdGValA6WiuokrNKqjrTNsy5ABWU11bdKOzUdheteXg==",
"path": "system.numerics.vectors/4.6.0",
"hashPath": "system.numerics.vectors.4.6.0.nupkg.sha512"
},
"DungeonMapGenerator/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
}
}
}

View file

@ -0,0 +1,12 @@
{
"runtimeOptions": {
"tfm": "net9.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "9.0.0"
},
"configProperties": {
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
}
}
}

View file

@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace DungeonMapGenerator
{
public static class CollectionExtensions
{
public static void AddRange<T>(this ICollection<T> collection, IEnumerable<T> items)
{
foreach (var item in items)
{
collection.Add(item);
}
}
}
}

View file

@ -0,0 +1,636 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Random = System.Random;
namespace DungeonMapGenerator
{
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<Room> rootRooms = new List<Room>(dungeon.GetMonsterRooms());
rootRooms.Add(dungeon.GetBossRoom());
// Create list of connection rooms
Dictionary<RoomSide, List<Room>> nodeRoomAndSideItIsOn = dungeon.GetNodesWithSides();
List<Room> nodeRoomsWithEntrances = new List<Room>(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)
{
foreach (Room room in roomSideWithRoom.Value)
{
Room closestSeenRoom = FindClosestSeenConnection(room,
roomSideWithRoom.Key,
nodeRoomsWithEntrances,
rootRooms);
if (closestSeenRoom != null)
{
//Place rooms along the line until they are connected
dungeon.AddRooms(PlaceRoomsOrganicallyTowardRoom(room, closestSeenRoom, RoomType.Normal,
dungeon.GetOccupiedPoints(), dungeon.GetUnoccupiedPoints()));
}
}
}
}
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<Point> topUnoccupiedPoints = new List<Point>(GetUnoccupiedPointsOnSide(room, RoomSide.Top, dungeon.GetUnoccupiedPoints()));
List<Point> bottomUnoccupiedPoints = new List<Point>(GetUnoccupiedPointsOnSide(room, RoomSide.Bottom, dungeon.GetUnoccupiedPoints()));
List<Point> leftUnoccupiedPoints = new List<Point>(GetUnoccupiedPointsOnSide(room, RoomSide.Left, dungeon.GetUnoccupiedPoints()));
List<Point> rightUnoccupiedPoints = new List<Point>(GetUnoccupiedPointsOnSide(room, RoomSide.Right, dungeon.GetUnoccupiedPoints()));
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))
{
List<Point> unoccupiedPointsOnSide;
switch (side)
{
case RoomSide.Top:
unoccupiedPointsOnSide = topUnoccupiedPoints;
break;
case RoomSide.Bottom:
unoccupiedPointsOnSide = bottomUnoccupiedPoints;
break;
case RoomSide.Left:
unoccupiedPointsOnSide = leftUnoccupiedPoints;
break;
case RoomSide.Right:
unoccupiedPointsOnSide = rightUnoccupiedPoints;
break;
default:
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<Point> GetUnoccupiedPointsOnSide(Room room, RoomSide side, HashSet<Point> unoccupiedPoints)
{
List<Point> unoccupiedPointsOnSide = new List<Point>();
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<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;
switch (side)
{
case RoomSide.Top:
adjustedPoint = new Point(firstPoint.X, firstPoint.Y); // Move up for top side
break;
case RoomSide.Bottom:
adjustedPoint = new Point(firstPoint.X, firstPoint.Y - 1); // Move down for bottom side
break;
case RoomSide.Left:
adjustedPoint = new Point(firstPoint.X, firstPoint.Y); // Move left for left side
break;
case RoomSide.Right:
adjustedPoint = new Point(firstPoint.X - 1, firstPoint.Y); // Move right for right side
break;
default:
adjustedPoint = firstPoint; // Default case (shouldn't happen)
break;
}
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 Dictionary<Side, int>()
{
{ 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<Room> possibleConnections,
List<Room> obstructions)
{
const float minClosestLength = 0;
Vector2 lookDirection;
switch (attachedSide)
{
case RoomSide.Top:
lookDirection = new Vector2(0, -1); // Expanding upward
break;
case RoomSide.Bottom:
lookDirection = new Vector2(0, 1); // Expanding downward
break;
case RoomSide.Left:
lookDirection = new Vector2(-1, 0); // Expanding left
break;
case RoomSide.Right:
lookDirection = new Vector2(1, 0); // Expanding right
break;
default:
lookDirection = Vector2.Zero; // Default (should not happen)
break;
}
Point nodeCenter = nodeRoom.GetCenterOfRoom();
Double visionConeAngle = Math.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);
Double angle = Math.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<Room> obstructions)
{
List<Point> linePoints = GetPointsOnLine(start, end);
foreach (Point point in linePoints)
{
if (obstructions.Any(room => room.ContainsPoint(point)))
{
return true;
}
}
return false;
}
// Bresenhams Line Algorithm to get points between two points
private List<Point> GetPointsOnLine(Point p0, Point p1)
{
List<Point> points = new List<Point>();
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<Room> PlaceRoomsOrganicallyTowardRoom(Room startingRoom, Room targetRoom, RoomType type,
HashSet<Point> occupiedPoints, HashSet<Point> unoccupiedPoints)
{
List<Room> placedRooms = new List<Room>();
Room currentRoom = startingRoom;
// TODO I don't think I actually need this
List<Point> path = GetPointsOnLine(startingRoom.GetCenterOfRoom(), targetRoom.GetCenterOfRoom());
bool roomPlaced = true;
while (currentRoom.GetCenterOfRoom().ManhattanDistance(targetRoom.GetCenterOfRoom()) >=
GetRoomSizeByType(type) && roomPlaced)
{
Room newRoom;
// Get sides of room to try and place new room
(RoomSide primary, RoomSide secondary) =
GetSidesTowardsEndPoint(currentRoom.GetCenterOfRoom(), targetRoom.GetCenterOfRoom());
List<Point> availablePointsOnSide = GetUnoccupiedPointsOnSide(currentRoom, primary, unoccupiedPoints);
newRoom = CreateAdjacentRoom(type, availablePointsOnSide, primary, occupiedPoints);
// If the primary side doesn't have room for a new room the try secondary
if (newRoom == null)
{
availablePointsOnSide = GetUnoccupiedPointsOnSide(currentRoom, secondary, unoccupiedPoints);
newRoom = CreateAdjacentRoom(type, availablePointsOnSide, secondary, occupiedPoints);
}
if (newRoom != null)
{
placedRooms.Add(newRoom);
occupiedPoints.AddRange(newRoom.GetPointsInRoom());
currentRoom = newRoom;
}
else
{
roomPlaced = false;
}
}
return placedRooms;
}
public (RoomSide primarySide, RoomSide secondarySide) GetSidesTowardsEndPoint(Point start, Point end)
{
// Calculate the differences in X and Y
int deltaX = end.X - start.X;
int deltaY = end.Y - start.Y;
// Determine the primary side
RoomSide primarySide;
RoomSide secondarySide;
if (Math.Abs(deltaX) > Math.Abs(deltaY))
{
// If horizontal movement is greater, primary side is horizontal
primarySide = deltaX > 0 ? RoomSide.Right : RoomSide.Left;
secondarySide = deltaY > 0 ? RoomSide.Bottom : RoomSide.Top;
}
else
{
// If vertical movement is greater, primary side is vertical
primarySide = deltaY > 0 ? RoomSide.Bottom : RoomSide.Top;
secondarySide = deltaX > 0 ? RoomSide.Right : RoomSide.Left;
}
return (primarySide, secondarySide);
}
} // Class
} // Namespace

View file

@ -0,0 +1,196 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace DungeonMapGenerator
{
public class DungeonMap
{
private int _width;
private int _height;
private List<Room> _monsterRooms = new List<Room>();
private List<Room> _entranceRooms = new List<Room>();
private List<Room> _normalRooms = new List<Room>();
private Room _bossRoom;
private HashSet<Point> _unoccupiedPoints = new HashSet<Point>();
private HashSet<Point> _occupiedPoints = new HashSet<Point>();
public DungeonMap(int width, int height)
{
_width = width;
_height = height;
_unoccupiedPoints.AddRange(Enumerable.Range(0, _width)
.SelectMany(x => Enumerable.Range(0, _height)
.Select(y => new Point(x,y))));
}
public void AddRoom(Room room)
{
switch (room.TypeOfRoom)
{
case RoomType.Monster:
_monsterRooms.Add(room);
break;
case RoomType.Entrance:
_entranceRooms.Add(room);
break;
case RoomType.Normal:
_normalRooms.Add(room);
break;
case RoomType.Boss:
_bossRoom = room;
break;
default:
return;
}
AddPointsToOccupied(room.GetPointsInRoom());
}
public void AddRooms(List<Room> rooms)
{
if (rooms.Count == 0) return;
switch (rooms[0].TypeOfRoom)
{
case RoomType.Monster:
_monsterRooms.AddRange(rooms);
break;
case RoomType.Entrance:
_entranceRooms.AddRange(rooms);
break;
case RoomType.Normal:
_normalRooms.AddRange(rooms);
break;
default:
return;
}
AddPointsToOccupied(rooms.SelectMany(room => room.GetPointsInRoom()).ToList());
}
public List<Room> GetMonsterRooms()
{
return _monsterRooms;
}
public Room GetBossRoom()
{
return _bossRoom;
}
public List<Room> GetEntranceRooms()
{
return _entranceRooms;
}
public List<Room> GetNormalRooms()
{
return _normalRooms;
}
public HashSet<Point> GetUnoccupiedPoints()
{
return new HashSet<Point>(_unoccupiedPoints);
}
public HashSet<Point> GetOccupiedPoints()
{
return new HashSet<Point>(_occupiedPoints);
}
public string GetMapAsString()
{
List<Point> monsterRoomPoints = _monsterRooms.SelectMany(room => room.GetPointsInRoom()).ToList();
List<Point> entranceRoomPoints = _entranceRooms.SelectMany(room => room.GetPointsInRoom()).ToList();
List<Point> normalRoomPoints = _normalRooms.SelectMany(room => room.GetPointsInRoom()).ToList();
char[,] mapMatrix = new Char[_width, _height];
for (int x = 0; x < _width; x++)
{
for (int y = 0; y < _height; y++)
{
Point point = new Point(x, y);
if (entranceRoomPoints.Contains(point))
{
mapMatrix[x, y] = '*';
}
else if (monsterRoomPoints.Contains(point))
{
mapMatrix[x, y] = 'X';
}
else if (_bossRoom.GetPointsInRoom().Contains(point))
{
mapMatrix[x, y] = 'B';
}
else if (normalRoomPoints.Contains(point))
{
mapMatrix[x, y] = 'N';
}
else
{
mapMatrix[x, y] = '-';
}
}
}
StringBuilder sb = new StringBuilder();
for (int j = 0; j < mapMatrix.GetLength(1); j++)
{
for (int i = 0; i < mapMatrix.GetLength(0); i++)
{
sb.Append(mapMatrix[i, j] + " ");
}
sb.Append(Environment.NewLine);
}
return $"{DateTime.Now}{Environment.NewLine}" +
$"X Length: {_width}{Environment.NewLine}" +
$"Y Length: {_height}{Environment.NewLine}" +
$"Monster Rooms: {_monsterRooms.Count}{Environment.NewLine}" +
sb.ToString();
}
private void AddPointsToOccupied(List<Point> points)
{
_occupiedPoints.AddRange(points);
_unoccupiedPoints.ExceptWith(points);
}
private void RemovePointsFromOccupied(List<Point> points)
{
_occupiedPoints.ExceptWith(points);
_unoccupiedPoints.AddRange(points);
}
// Node rooms are the rooms placed around each monster room and the boss room.
public List<Room> GetNodeRooms()
{
List<Room> nodeRooms = new List<Room>();
nodeRooms.AddRange(_monsterRooms.SelectMany(room => room.GetAdjacentRooms()));
nodeRooms.AddRange(_bossRoom.GetAdjacentRooms());
return nodeRooms;
}
public Dictionary<RoomSide, List<Room>> GetNodesWithSides()
{
Dictionary<RoomSide, List<Room>> nodesWithSides = new Dictionary<RoomSide, List<Room>>();
foreach (Room room in _monsterRooms)
{
foreach (var kvp in room.GetAdjacentRoomsDict())
{
if (!nodesWithSides.ContainsKey(kvp.Key))
{
nodesWithSides[kvp.Key] = new List<Room>(kvp.Value);
}
else
{
nodesWithSides[kvp.Key].AddRange(kvp.Value);
}
}
}
return nodesWithSides;
}
}
}

View file

@ -0,0 +1,46 @@
using System;
using System.IO;
using Newtonsoft.Json;
namespace DungeonMapGenerator
{
public class DungeonMapSerializer
{
// Serialize DungeonMap to JSON file
public static void SerializeToFile(DungeonMap dungeonMap, string filePath)
{
try
{
// Convert DungeonMap object to JSON string
string json = JsonConvert.SerializeObject(dungeonMap, Formatting.Indented);
// Write JSON string to a file
File.WriteAllText(filePath, json);
}
catch (Exception ex)
{
Console.WriteLine($"Error during serialization: {ex.Message}");
}
}
// Deserialize DungeonMap from JSON file
public static DungeonMap DeserializeFromFile(string filePath)
{
try
{
// Read JSON string from file
string json = File.ReadAllText(filePath);
// Convert JSON string to DungeonMap object
DungeonMap dungeonMap = JsonConvert.DeserializeObject<DungeonMap>(json);
return dungeonMap;
}
catch (Exception ex)
{
Console.WriteLine($"Error during deserialization: {ex.Message}");
return null;
}
}
}
}

View file

@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Random = System.Random;
namespace DungeonMapGenerator
{
public class EvenDisperser
{
private List<Room> _samples = new List<Room>(){new Room(
RoomType.Normal,
1,
1,
new Point(1000, 1000))};
private HashSet<Point> _availablePoints;
public EvenDisperser(int xLength, int yLength, HashSet<Point> availablePoints)
{
_availablePoints = availablePoints;
}
private HashSet<Point> GenerateAvailablePoints(int xLength, int yLength, List<Point> excludedPoints)
{
HashSet<Point> availablePoints = new HashSet<Point>();
for (var x = 0; x < xLength; x++)
{
for (var y = 0; y < yLength; y++)
{
if (!excludedPoints.Contains(new Point(x, y)))
{
availablePoints.Add(new Point(x, y));
}
}
}
return availablePoints;
}
public Room GenerateAndPlaceRoom(int xLength, int yLength, RoomType roomType)
{
int numCandidates = 100; // Increasing improves results but greatly effects performance.
Random rnd = new Random();
Room bestCandidate = new Room(roomType, xLength, yLength, new Point(Int32.MaxValue, Int32.MaxValue));
int bestDistance = 0;
for (var i = 0; i < numCandidates; i++)
{
var candidate = new Room(
roomType, xLength, yLength, _availablePoints.ToList()[rnd.Next(0, _availablePoints.Count)]);
var distance = candidate.GetDistanceToRoom(FindClosestRoom(_samples, candidate));
if (distance > bestDistance
&& candidate.GetPointsInRoom().All(room => _availablePoints.Contains(room)))
{
_availablePoints.ExceptWith(candidate.GetPointsInRoom());
bestCandidate = candidate;
bestDistance = distance;
}
}
_samples.Add(bestCandidate);
return bestCandidate;
}
private Room FindClosestRoom(List<Room> options, Room reference)
{
Room closest = options[0];
foreach (var option in options)
{
if (reference.GetDistanceToRoom(option) < reference.GetDistanceToRoom(closest))
{
closest = option;
}
}
return closest;
}
}
}

View file

@ -0,0 +1,25 @@
using System;
namespace DungeonMapGenerator
{
public struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) {
X = x;
Y = y;
}
public override bool Equals(object obj) => obj is Point p && X == p.X && Y == p.Y;
public override int GetHashCode() => (X, Y).GetHashCode();
public override string ToString() => $"({X}, {Y})";
public static Point operator +(Point a, Point b) => new Point(a.X + b.X, a.Y + b.Y);
public static Point operator -(Point a, Point b) => new Point(a.X - b.X, a.Y - b.Y);
public int ManhattanDistance(Point other) => Math.Abs(X - other.X) + Math.Abs(Y - other.Y);
}
}

View file

@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace DungeonMapGenerator
{
public enum RoomSide
{
Top,
Bottom,
Left,
Right
}
public enum RoomType
{
Normal,
Entrance,
Monster,
Boss
}
public class Room
{
public Room(RoomType roomType, int width, int height, Point positionOfTopLeft)
{
TypeOfRoom = roomType;
Width = width;
Height = height;
PositionOfTopLeft = positionOfTopLeft;
}
private readonly Dictionary<RoomSide, List<Room>> _adjacentRooms = new Dictionary<RoomSide, List<Room>>();
public RoomType TypeOfRoom { get; set; }
public int Height { get; set; }
public int Width { get; set; }
public Point PositionOfTopLeft { get; set; }
public List<Point> GetPointsInRoom()
{
List<Point> points = new List<Point>();
for (int i = 0; i < Width; i++)
{
for (int j = 0; j < Height; j++)
{
points.Add(new Point(PositionOfTopLeft.X + i, PositionOfTopLeft.Y + j));
}
}
return points;
}
public bool ContainsPoint(Point point)
{
return GetPointsInRoom().Any(p => p.Equals(point));
}
public int GetDistanceToRoom(Room other)
{
Point centerOfRoom = GetCenterOfRoom();
Point centerOfOther = other.GetCenterOfRoom();
return centerOfRoom.ManhattanDistance(centerOfOther);
}
public Point GetCenterOfRoom()
{
return new Point(PositionOfTopLeft.X + Width / 2, PositionOfTopLeft.Y + Height / 2);
}
public void AddAdjacentRoom(Room room, RoomSide side)
{
if (!_adjacentRooms.ContainsKey(side))
{
_adjacentRooms[side] = new List<Room>();
}
// Prevent duplicates
if (!_adjacentRooms[side].Contains(room))
{
_adjacentRooms[side].Add(room);
room.AddAdjacentRoom(this, GetOppositeSide(side)); // Ensure bidirectional linkage
}
}
public IEnumerable<Room> GetAdjacentRooms()
{
return _adjacentRooms.Values.SelectMany(roomList => roomList);
}
public Dictionary<RoomSide, List<Room>> GetAdjacentRoomsDict()
{
return new Dictionary<RoomSide, List<Room>>(_adjacentRooms);
}
// Helper method to get the opposite side
private RoomSide GetOppositeSide(RoomSide side)
{
switch (side)
{
case RoomSide.Top:
return RoomSide.Bottom;
case RoomSide.Bottom:
return RoomSide.Top;
case RoomSide.Left:
return RoomSide.Right;
case RoomSide.Right:
return RoomSide.Left;
default:
throw new ArgumentException("Invalid RoomSide", nameof(side));
}
}
}
}

View file

@ -0,0 +1,79 @@
{
"runtimeTarget": {
"name": ".NETStandard,Version=v2.0/",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETStandard,Version=v2.0": {},
".NETStandard,Version=v2.0/": {
"DungeonMapGenerator/1.0.0": {
"dependencies": {
"NETStandard.Library": "2.0.3",
"Newtonsoft.Json": "13.0.3",
"System.Numerics.Vectors": "4.6.0"
},
"runtime": {
"DungeonMapGenerator.dll": {}
}
},
"Microsoft.NETCore.Platforms/1.1.0": {},
"NETStandard.Library/2.0.3": {
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0"
}
},
"Newtonsoft.Json/13.0.3": {
"runtime": {
"lib/netstandard2.0/Newtonsoft.Json.dll": {
"assemblyVersion": "13.0.0.0",
"fileVersion": "13.0.3.27908"
}
}
},
"System.Numerics.Vectors/4.6.0": {
"runtime": {
"lib/netstandard2.0/System.Numerics.Vectors.dll": {
"assemblyVersion": "4.1.3.0",
"fileVersion": "4.600.24.56208"
}
}
}
}
},
"libraries": {
"DungeonMapGenerator/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Microsoft.NETCore.Platforms/1.1.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==",
"path": "microsoft.netcore.platforms/1.1.0",
"hashPath": "microsoft.netcore.platforms.1.1.0.nupkg.sha512"
},
"NETStandard.Library/2.0.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
"path": "netstandard.library/2.0.3",
"hashPath": "netstandard.library.2.0.3.nupkg.sha512"
},
"Newtonsoft.Json/13.0.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==",
"path": "newtonsoft.json/13.0.3",
"hashPath": "newtonsoft.json.13.0.3.nupkg.sha512"
},
"System.Numerics.Vectors/4.6.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-t+SoieZsRuEyiw/J+qXUbolyO219tKQQI0+2/YI+Qv7YdGValA6WiuokrNKqjrTNsy5ABWU11bdKOzUdheteXg==",
"path": "system.numerics.vectors/4.6.0",
"hashPath": "system.numerics.vectors.4.6.0.nupkg.sha512"
}
}
}