CSharp Example of a Dungeon-Building Algorithm
Jump to navigation
Jump to search
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.
namespace DungeonGenerator.Java
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;
public class Dungeon
{
// misc. messages to print
const string MsgXSize = "X size of dungeon: \t";
const string MsgYSize = "Y size of dungeon: \t";
const string MsgMaxObjects = "max # of objects: \t";
const string MsgNumObjects = "# of objects made: \t";
// max size of the map
int xmax = 80; //columns
int ymax = 25; //rows
// size of the map
int _xsize;
int _ysize;
// number of "objects" to generate on the map
int _objects;
// define the %chance to generate either a room or a corridor on the map
// BTW, rooms are 1st priority so actually it's enough to just define the chance
// of generating a room
const int ChanceRoom = 75;
// our map
Tile[] _dungeonMap = { };
readonly IRandomize _rnd;
readonly Action<string> _logger;
public Dungeon(IRandomize rnd, Action<string> logger)
{
_rnd = rnd;
_logger = logger;
}
public int Corridors
{
get;
private set;
}
public static bool IsWall(int x, int y, int xlen, int ylen, int xt, int yt, Direction d)
{
Func<int, int, int> a = GetFeatureLowerBound;
Func<int, int, int> b = IsFeatureWallBound;
switch (d)
{
case Direction.North:
return xt == a(x, xlen) || xt == b(x, xlen) || yt == y || yt == y - ylen + 1;
case Direction.East:
return xt == x || xt == x + xlen - 1 || yt == a(y, ylen) || yt == b(y, ylen);
case Direction.South:
return xt == a(x, xlen) || xt == b(x, xlen) || yt == y || yt == y + ylen - 1;
case Direction.West:
return xt == x || xt == x - xlen + 1 || yt == a(y, ylen) || yt == b(y, ylen);
}
throw new InvalidOperationException();
}
public static int GetFeatureLowerBound(int c, int len)
{
return c - len / 2;
}
public static int IsFeatureWallBound(int c, int len)
{
return c + (len - 1) / 2;
}
public static int GetFeatureUpperBound(int c, int len)
{
return c + (len + 1) / 2;
}
public static IEnumerable<PointI> GetRoomPoints(int x, int y, int xlen, int ylen, Direction d)
{
// north and south share the same x strategy
// east and west share the same y strategy
Func<int, int, int> a = GetFeatureLowerBound;
Func<int, int, int> b = GetFeatureUpperBound;
switch (d)
{
case Direction.North:
for (var xt = a(x, xlen); xt < b(x, xlen); xt++) for (var yt = y; yt > y - ylen; yt--) yield return new PointI { X = xt, Y = yt };
break;
case Direction.East:
for (var xt = x; xt < x + xlen; xt++) for (var yt = a(y, ylen); yt < b(y, ylen); yt++) yield return new PointI { X = xt, Y = yt };
break;
case Direction.South:
for (var xt = a(x, xlen); xt < b(x, xlen); xt++) for (var yt = y; yt < y + ylen; yt++) yield return new PointI { X = xt, Y = yt };
break;
case Direction.West:
for (var xt = x; xt > x - xlen; xt--) for (var yt = a(y, ylen); yt < b(y, ylen); yt++) yield return new PointI { X = xt, Y = yt };
break;
default:
yield break;
}
}
public Tile GetCellType(int x, int y)
{
try
{
return this._dungeonMap[x + this._xsize * y];
}
catch (IndexOutOfRangeException)
{
new { x, y }.Dump("exceptional");
throw;
}
}
public int GetRand(int min, int max)
{
return _rnd.Next(min, max);
}
public bool MakeCorridor(int x, int y, int length, Direction direction)
{
// define the dimensions of the corridor (er.. only the width and height..)
int len = this.GetRand(2, length);
const Tile Floor = Tile.Corridor;
int xtemp;
int ytemp = 0;
switch (direction)
{
case Direction.North:
// north
// check if there's enough space for the corridor
// start with checking it's not out of the boundaries
if (x < 0 || x > this._xsize) return false;
xtemp = x;
// same thing here, to make sure it's not out of the boundaries
for (ytemp = y; ytemp > (y - len); ytemp--)
{
if (ytemp < 0 || ytemp > this._ysize) return false; // oh boho, it was!
if (GetCellType(xtemp, ytemp) != Tile.Unused) return false;
}
// if we're still here, let's start building
Corridors++;
for (ytemp = y; ytemp > (y - len); ytemp--)
{
this.SetCell(xtemp, ytemp, Floor);
}
break;
case Direction.East:
// east
if (y < 0 || y > this._ysize) return false;
ytemp = y;
for (xtemp = x; xtemp < (x + len); xtemp++)
{
if (xtemp < 0 || xtemp > this._xsize) return false;
if (GetCellType(xtemp, ytemp) != Tile.Unused) return false;
}
Corridors++;
for (xtemp = x; xtemp < (x + len); xtemp++)
{
this.SetCell(xtemp, ytemp, Floor);
}
break;
case Direction.South:
// south
if (x < 0 || x > this._xsize) return false;
xtemp = x;
for (ytemp = y; ytemp < (y + len); ytemp++)
{
if (ytemp < 0 || ytemp > this._ysize) return false;
if (GetCellType(xtemp, ytemp) != Tile.Unused) return false;
}
Corridors++;
for (ytemp = y; ytemp < (y + len); ytemp++)
{
this.SetCell(xtemp, ytemp, Floor);
}
break;
case Direction.West:
// west
if (ytemp < 0 || ytemp > this._ysize) return false;
ytemp = y;
for (xtemp = x; xtemp > (x - len); xtemp--)
{
if (xtemp < 0 || xtemp > this._xsize) return false;
if (GetCellType(xtemp, ytemp) != Tile.Unused) return false;
}
Corridors++;
for (xtemp = x; xtemp > (x - len); xtemp--)
{
this.SetCell(xtemp, ytemp, Floor);
}
break;
}
// woot, we're still here! let's tell the other guys we're done!!
return true;
}
public IEnumerable<Tuple<PointI, Direction>> GetSurroundingPoints(PointI v)
{
var points = new[]
{
Tuple.Create(new PointI { X = v.X, Y = v.Y + 1 }, Direction.North),
Tuple.Create(new PointI { X = v.X - 1, Y = v.Y }, Direction.East),
Tuple.Create(new PointI { X = v.X , Y = v.Y-1 }, Direction.South),
Tuple.Create(new PointI { X = v.X +1, Y = v.Y }, Direction.West),
};
return points.Where(p => InBounds(p.Item1));
}
public IEnumerable<Tuple<PointI, Direction, Tile>> GetSurroundings(PointI v)
{
return
this.GetSurroundingPoints(v)
.Select(r => Tuple.Create(r.Item1, r.Item2, this.GetCellType(r.Item1.X, r.Item1.Y)));
}
public bool InBounds(int x, int y)
{
return x > 0 && x < this.xmax && y > 0 && y < this.ymax;
}
public bool InBounds(PointI v)
{
return this.InBounds(v.X, v.Y);
}
public bool MakeRoom(int x, int y, int xlength, int ylength, Direction direction)
{
// define the dimensions of the room, it should be at least 4x4 tiles (2x2 for walking on, the rest is walls)
int xlen = this.GetRand(4, xlength);
int ylen = this.GetRand(4, ylength);
// the tile type it's going to be filled with
const Tile Floor = Tile.DirtFloor;
const Tile Wall = Tile.DirtWall;
// choose the way it's pointing at
var points = GetRoomPoints(x, y, xlen, ylen, direction).ToArray();
// Check if there's enough space left for it
if (
points.Any(
s =>
s.Y < 0 || s.Y > this._ysize || s.X < 0 || s.X > this._xsize || this.GetCellType(s.X, s.Y) != Tile.Unused)) return false;
_logger(
string.Format(
"Making room:int x={0}, int y={1}, int xlength={2}, int ylength={3}, int direction={4}",
x,
y,
xlength,
ylength,
direction));
foreach (var p in points)
{
this.SetCell(p.X, p.Y, IsWall(x, y, xlen, ylen, p.X, p.Y, direction) ? Wall : Floor);
}
// yay, all done
return true;
}
public Tile[] GetDungeon()
{
return this._dungeonMap;
}
public char GetCellTile(int x, int y)
{
switch (GetCellType(x, y))
{
case Tile.Unused:
return '';
case Tile.DirtWall:
return '|';
case Tile.DirtFloor:
return '_';
case Tile.StoneWall:
return 'S';
case Tile.Corridor:
return '#';
case Tile.Door:
return 'D';
case Tile.Upstairs:
return '+';
case Tile.Downstairs:
return '-';
case Tile.Chest:
return 'C';
default:
throw new ArgumentOutOfRangeException("x,y");
}
}
//used to print the map on the screen
public void ShowDungeon()
{
for (int y = 0; y < this._ysize; y++)
{
for (int x = 0; x < this._xsize; x++)
{
Console.Write(GetCellTile(x, y));
}
if (this._xsize <= xmax) Console.WriteLine();
}
}
public Direction RandomDirection()
{
int dir = this.GetRand(0, 4);
switch (dir)
{
case 0:
return Direction.North;
case 1:
return Direction.East;
case 2:
return Direction.South;
case 3:
return Direction.West;
default:
throw new InvalidOperationException();
}
}
//and here's the one generating the whole map
public bool CreateDungeon(int inx, int iny, int inobj)
{
this._objects = inobj < 1 ? 10 : inobj;
// adjust the size of the map, if it's smaller or bigger than the limits
if (inx < 3) this._xsize = 3;
else if (inx > xmax) this._xsize = xmax;
else this._xsize = inx;
if (iny < 3) this._ysize = 3;
else if (iny > ymax) this._ysize = ymax;
else this._ysize = iny;
Console.WriteLine(MsgXSize + this._xsize);
Console.WriteLine(MsgYSize + this._ysize);
Console.WriteLine(MsgMaxObjects + this._objects);
// redefine the map var, so it's adjusted to our new map size
this._dungeonMap = new Tile[this._xsize * this._ysize];
// start with making the "standard stuff" on the map
this.Initialize();
/*******************************************************************************
And now the code of the random-map-generation-algorithm begins!
*******************************************************************************/
// start with making a room in the middle, which we can start building upon
this.MakeRoom(this._xsize / 2, this._ysize / 2, 8, 6, RandomDirection()); // getrand saken f????r att slumpa fram riktning p?? rummet
// keep count of the number of "objects" we've made
int currentFeatures = 1; // +1 for the first room we just made
// then we sart the main loop
for (int countingTries = 0; countingTries < 1000; countingTries++)
{
// check if we've reached our quota
if (currentFeatures == this._objects)
{
break;
}
// start with a random wall
int newx = 0;
int xmod = 0;
int newy = 0;
int ymod = 0;
Direction? validTile = null;
// 1000 chances to find a suitable object (room or corridor)..
for (int testing = 0; testing < 1000; testing++)
{
newx = this.GetRand(1, this._xsize - 1);
newy = this.GetRand(1, this._ysize - 1);
if (GetCellType(newx, newy) == Tile.DirtWall || GetCellType(newx, newy) == Tile.Corridor)
{
var surroundings = this.GetSurroundings(new PointI() { X = newx, Y = newy });
// check if we can reach the place
var canReach =
surroundings.FirstOrDefault(s => s.Item3 == Tile.Corridor || s.Item3 == Tile.DirtFloor);
if (canReach == null)
{
continue;
}
validTile = canReach.Item2;
switch (canReach.Item2)
{
case Direction.North:
xmod = 0;
ymod = -1;
break;
case Direction.East:
xmod = 1;
ymod = 0;
break;
case Direction.South:
xmod = 0;
ymod = 1;
break;
case Direction.West:
xmod = -1;
ymod = 0;
break;
default:
throw new InvalidOperationException();
}
// check that we haven't got another door nearby, so we won't get alot of openings besides
// each other
if (GetCellType(newx, newy + 1) == Tile.Door) // north
{
validTile = null;
}
else if (GetCellType(newx - 1, newy) == Tile.Door) // east
validTile = null;
else if (GetCellType(newx, newy - 1) == Tile.Door) // south
validTile = null;
else if (GetCellType(newx + 1, newy) == Tile.Door) // west
validTile = null;
// if we can, jump out of the loop and continue with the rest
if (validTile.HasValue) break;
}
}
if (validTile.HasValue)
{
// choose what to build now at our newly found place, and at what direction
int feature = this.GetRand(0, 100);
if (feature <= ChanceRoom)
{ // a new room
if (this.MakeRoom(newx + xmod, newy + ymod, 8, 6, validTile.Value))
{
currentFeatures++; // add to our quota
// then we mark the wall opening with a door
this.SetCell(newx, newy, Tile.Door);
// clean up infront of the door so we can reach it
this.SetCell(newx + xmod, newy + ymod, Tile.DirtFloor);
}
}
else if (feature >= ChanceRoom)
{ // new corridor
if (this.MakeCorridor(newx + xmod, newy + ymod, 6, validTile.Value))
{
// same thing here, add to the quota and a door
currentFeatures++;
this.SetCell(newx, newy, Tile.Door);
}
}
}
}
/*******************************************************************************
All done with the building, let's finish this one off
*******************************************************************************/
AddSprinkles();
// all done with the map generation, tell the user about it and finish
Console.WriteLine(MsgNumObjects + currentFeatures);
return true;
}
void Initialize()
{
for (int y = 0; y < this._ysize; y++)
{
for (int x = 0; x < this._xsize; x++)
{
// ie, making the borders of unwalkable walls
if (y == 0 || y == this._ysize - 1 || x == 0 || x == this._xsize - 1)
{
this.SetCell(x, y, Tile.StoneWall);
}
else
{ // and fill the rest with dirt
this.SetCell(x, y, Tile.Unused);
}
}
}
}
// setting a tile's type
void SetCell(int x, int y, Tile celltype)
{
this._dungeonMap[x + this._xsize * y] = celltype;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void AddSprinkles()
{
// sprinkle out the bonusstuff (stairs, chests etc.) over the map
int state = 0; // the state the loop is in, start with the stairs
while (state != 10)
{
for (int testing = 0; testing < 1000; testing++)
{
var newx = this.GetRand(1, this._xsize - 1);
int newy = this.GetRand(1, this._ysize - 2);
// Console.WriteLine("x: " + newx + "\ty: " + newy);
int ways = 4; // from how many directions we can reach the random spot from
// check if we can reach the spot
if (GetCellType(newx, newy + 1) == Tile.DirtFloor || GetCellType(newx, newy + 1) == Tile.Corridor)
{
// north
if (GetCellType(newx, newy + 1) != Tile.Door)
ways--;
}
if (GetCellType(newx - 1, newy) == Tile.DirtFloor || GetCellType(newx - 1, newy) == Tile.Corridor)
{
// east
if (GetCellType(newx - 1, newy) != Tile.Door)
ways--;
}
if (GetCellType(newx, newy - 1) == Tile.DirtFloor || GetCellType(newx, newy - 1) == Tile.Corridor)
{
// south
if (GetCellType(newx, newy - 1) != Tile.Door)
ways--;
}
if (GetCellType(newx + 1, newy) == Tile.DirtFloor || GetCellType(newx + 1, newy) == Tile.Corridor)
{
// west
if (GetCellType(newx + 1, newy) != Tile.Door)
ways--;
}
if (state == 0)
{
if (ways == 0)
{
// we're in state 0, let's place a "upstairs" thing
this.SetCell(newx, newy, Tile.Upstairs);
state = 1;
break;
}
}
else if (state == 1)
{
if (ways == 0)
{
// state 1, place a "downstairs"
this.SetCell(newx, newy, Tile.Downstairs);
state = 10;
break;
}
}
}
}
}
}
}