CSharp Example of a Dungeon-Building Algorithm

From RogueBasin
Revision as of 09:36, 26 June 2014 by McLovin (talk | contribs)
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;
                       }
                   }
               }
           }
       }
     }
   }