CSharp · Console
//
// BSP Room Generator v0.12
// By Fangwei Lee
//
using System;
using System.Diagnostics;
using System.Drawing;
using System.Collections.Generic;
public struct Config
{
public const int SEED = 7777;
public const int MAP_WIDTH = 40;
public const int MAP_HEIGHT =15;
public const int MAX_LEAVES_COUNT = 100;
public const int MIN_SIZE = 4; // so the min room size is N-1
public const int MAX_SIZE = 20;
public const float QUIT_RATE = 0.4f; // the possibility to quit splitting once big enough
public const int SINGLE_DOOR_PROB = 2;
public const int DOUBLE_DOOR_PROB = 1;
public const int HALLWAY_DOOR_PROB = 2;
}
public struct R
{
public static Random r;
}
//
// Main Program
//
public class Program
{
//
// Main Function
//
public static void Main()
{
Console.WriteLine("Room Generator v0.12 By Fangwei Lee\n");
// Initialize random stream
R.r = new Random(Config.SEED);
// Create a tree object and assign dimension
Tree tree = new Tree(1,1,Config.MAP_WIDTH-1,Config.MAP_HEIGHT-1);
// Draw the dwelling
tree.drawRooms();
// Print all rooms from a tree
tree.printRooms();
}
}
//
// BSP Tree Class
//
public class Tree
{
public Leaf root;
public List<Leaf> tree = new List<Leaf>();
public List<Room> rooms = new List<Room>();
public Tree(int x, int y, int width, int height)
{
// Create root
root = new Leaf(x, y, width, height);
tree.Add(root);
// Loop through leaves and split
int splitIdx = 0;
while (splitIdx < tree.Count && tree.Count < Config.MAX_LEAVES_COUNT)
{
Leaf toSplit = tree[splitIdx];
if( toSplit.split() ) //attempt to split
{
tree.Add( toSplit.leftChild );
tree.Add( toSplit.rightChild );
}
splitIdx ++;
}
// Now we can recursively travsal the tree and generate rooms at end leaves.
root.createRooms();
// Store rooms in list
foreach (Leaf leaf in tree)
if (leaf.room!=null)
rooms.Add(leaf.room);
}
//
// Print Rooms
//
public void printRooms()
{
Console.WriteLine("Room List:");
foreach (Room r in rooms)
{
Console.WriteLine(r.asString());
}
}
//
// Draw Rooms
//
public void drawRooms()
{
char[][] lines = new char[Config.MAP_WIDTH][];
for( int i = 0; i < Config.MAP_WIDTH; i++ )
{
lines[i] = new char[Config.MAP_HEIGHT];
for( int j = 0; j < Config.MAP_HEIGHT; j++ )
lines[i][j] = '#';
}
int dungeonCount = -1;
foreach( Leaf r in tree )
{
//Console.Write("hallcount="+r.halls.Count);
foreach (Room h in r.doors)
{
int area = h.rect.Width*h.rect.Height;
if (area == 1)
lines[h.rect.X][h.rect.Y] = '0';
else if (area == 2)
for (int i = 0; i<h.rect.Width; i++)
for (int j = 0; j<h.rect.Height; j++)
lines[h.rect.X+i][h.rect.Y+j] = '0';
else
for (int i = 0; i<h.rect.Width; i++)
for (int j = 0; j<h.rect.Height; j++)
lines[h.rect.X+i][h.rect.Y+j] = '.';
}
if (r.room==null)
continue;
Room d = r.room;
dungeonCount++;
for( int i = 0; i < d.rect.Width; i++ )
for( int j = 0; j < d.rect.Height; j++ )
lines[ d.rect.X+i ][ d.rect.Y+j ] = '.';
}
Console.Write("\\");
for( int i = 0; i < Config.MAP_WIDTH; i++ )
{
Console.Write(i%10);
}
Console.Write("\n");
for( int j = 0; j < Config.MAP_HEIGHT; j++ )
{
Console.Write(j%10);
for( int i = 0; i < Config.MAP_WIDTH; i++ )
Console.Write(lines[i][j]);
Console.Write("\n");
}
Console.WriteLine("\nLedgend:\n");
Console.WriteLine("#: Wall");
Console.WriteLine(".: Floor");
Console.WriteLine("0: Single Door");
Console.WriteLine("00: Double Door\n");
}
}
//
// BSP Leaf Class
//
public class Leaf
{
public uint id;
public int x, y, width, height, seed;
public Leaf parent, leftChild, rightChild;
//public Rectangle room = new Rectangle();
public Room room;
public List<Room> doors = new List<Room>();
//public List<Room> doors = new List<Room>();
public enum Direction:byte { Left=0/*0b00*/, Right=1/*0b01*/, Top=2/*0b10*/, Bottom=3/*0b11*/ };
public byte connectDir; // store which direction connect with it's sibling leaf
//
// Initializer
//
public Leaf(int inX, int inY, int inWidth, int inHeight, Leaf inParent=null, byte inConnectDir=0)
{
x = inX;
y = inY;
width = inWidth;
height = inHeight;
connectDir = inConnectDir;
parent = inParent;
if (parent == null)
{
id = 1;
}
else
{
if (connectDir == (byte)Direction.Top || connectDir == (byte)Direction.Left)
id = (parent.id<<1);
else
id = (parent.id<<1)+1;
}
Debug.WriteLine("id="+id); // print id
}
//
// Splitter
//
public bool split()
{
// Begin splitting the leaf into two children
if (leftChild != null || rightChild != null)
return false; // Bail out if it is alread splitted.
// Sometimes just don't split at all
if (R.r.NextDouble() < Config.QUIT_RATE && Math.Max(width, height) < Config.MAX_SIZE)
return false;
// Determine direction of split. Split on longer side for even distribution
bool isSplitTopBottom = height>width;
Debug.WriteLine(height+","+width+","+isSplitTopBottom);
// Determine max possible size to split
int max = (isSplitTopBottom?height:width) - Config.MIN_SIZE;
if (max <= Config.MIN_SIZE)
return false; // This dimension is too small to split
int splitPos = R.r.Next(Config.MIN_SIZE, max);
if (isSplitTopBottom)
{
leftChild = new Leaf(x, y, width, splitPos, this, (byte)Direction.Bottom); // leftChild is the top one
rightChild = new Leaf(x, y + splitPos, width, height - splitPos, this, (byte)Direction.Top);
}
else
{
leftChild = new Leaf(x, y, splitPos, height, this, (byte)Direction.Right); // leftChild is the left one
rightChild = new Leaf(x + splitPos, y, width - splitPos, height, this, (byte)Direction.Left);
}
return true; // We have made this far
}
//
// Create Rooms
//
public void createRooms()
{
//Console.WriteLine("generating rooms");
//Console.WriteLine(R.stream.Next(10));
if (leftChild != null || rightChild != null) //if current are has child areas, propagate the call
{
// this leaf has been split, so go into the children leafs
if (leftChild != null)
leftChild.createRooms();
if (rightChild != null)
rightChild.createRooms();
// if there are both left and right children in this Leaf, create a hallway between them
if (leftChild != null && rightChild != null)
{
//Rectangle leftRoom = leftChild.getRoomConnectToward(leftChild.connectDir);
Room leftRoom = leftChild.getRoomConnectToward(leftChild.connectDir);
createDoor(leftRoom, rightChild.getRoomNextTo(leftRoom));
}
}
else // if leaf node, create a dungeon within the minimum size constraints
{
int roomHeight = Math.Max(height, Config.MIN_SIZE )-1;
int roomWidth = Math.Max(width, Config.MIN_SIZE )-1;
// create the room
//room = new Rectangle(x, y, roomWidth, roomHeight);
room = new Room(x, y, roomWidth, roomHeight);
room.id = this.id;
Debug.WriteLine("new room:"+x+","+y+","+roomWidth+","+roomHeight);
}
}
//
// Travsal until find a room
//
public Room getRoomConnectToward(byte connectDir)
{
Debug.WriteLine(connectDir);
if (room!=null) // If the leaf has a room, usually an end leaf, return that room
{
return room;
}
else // Else divide and conquer its children
{
//Rectangle lRoom = new Rectangle();
//Rectangle rRoom = new Rectangle();
Room lRoom = new Room();
Room rRoom = new Room();
if (leftChild != null)
lRoom = leftChild.getRoomConnectToward(connectDir);
if (rightChild != null)
rRoom = rightChild.getRoomConnectToward(connectDir);
// Return room based on whether it's sibling is empty or not
// Otherwise, return the room that's closer to connection
if (lRoom==null && rRoom==null)
{
return null; // no room in this branch
}
else if (rRoom==null)
{
return lRoom; // if right room is empty, return left room
}
else if (lRoom==null)
{
return rRoom; // if left room is empty, return right room
}
else
{
// return the room that's closer to connection
switch (connectDir)
{
case (byte)Direction.Left: // if this leaf connect to the left
if (rRoom.rect.Left==lRoom.rect.Left)
return R.r.Next(2)==1?rRoom:lRoom; // pick random one if both children connect to the left
else
return rRoom.rect.Left<lRoom.rect.Left?rRoom:lRoom; // or pick the left most one
case (byte)Direction.Right: // if this leaf connect to the right
if (rRoom.rect.Right==lRoom.rect.Right)
return R.r.Next(2)==1?rRoom:lRoom; // pick random one if both children connect to the right
else
return (rRoom.rect.Right>lRoom.rect.Right?rRoom:lRoom); // or pick the right most one
case (byte)Direction.Top: // if this leaf connect to the top
if (rRoom.rect.Top==lRoom.rect.Top)
return R.r.Next(2)==1?rRoom:lRoom; // pick random one if both children connect to the top
else
return (rRoom.rect.Top<lRoom.rect.Top?rRoom:lRoom); // or pick the top most one
case (byte)Direction.Bottom: // if this leaf connect to the bottom
if (rRoom.rect.Bottom==lRoom.rect.Bottom)
return R.r.Next(2)==1?rRoom:lRoom; // pick random one if both children connect to the bottom
else
return (rRoom.rect.Bottom>lRoom.rect.Bottom?rRoom:lRoom); // or pick the bottom most one
default:
return R.r.Next(2)==1?rRoom:lRoom; // otherwise return random room.
}
}
}
}
//
// Travsal until find a room that next to input room
//
public Room getRoomNextTo(Room target)
{
if (room!=null)
{
return room;
}
else
{
Room lRoom = new Room();
Room rRoom = new Room();
if (leftChild != null)
lRoom = leftChild.getRoomNextTo(target);
if (rightChild != null)
rRoom = rightChild.getRoomNextTo(target);
if (getSharedWall(rRoom, target)!=null)
return rRoom;
else if (getSharedWall(lRoom, target)!=null)
return lRoom;
else
return null;
}
}
//
// Create door
//
public void createDoor(Room l, Room r)
{
Room sharedWall = getSharedWall(r, l);
if (sharedWall==null)
return; // no shared wall
bool isVertical = sharedWall.rect.Width < sharedWall.rect.Height;
int wallWidth = isVertical?sharedWall.rect.Height:sharedWall.rect.Width;
//int doorWidth = rnd.Next(1, wallWidth+1);
int doorWidth = getDoorWidth(wallWidth);
int margin = wallWidth - doorWidth;
Debug.WriteLine(wallWidth+", "+doorWidth+", "+margin);
if (margin<0)
return;
if (isVertical)
doors.Add(new Room(sharedWall.rect.X, sharedWall.rect.Y + R.r.Next(margin), 1, doorWidth));
else
doors.Add(new Room(sharedWall.rect.X + R.r.Next(margin), sharedWall.rect.Y, doorWidth, 1));
}
//
// shared wall
//
private Room getSharedWall(Room r, Room l)
{
if (r==null || l==null)
return null;
Room output = new Room(Rectangle.Intersect(Rectangle.Inflate(r.rect, 1, 1),Rectangle.Inflate(l.rect, 1, 1)));
if (output.rect.Width*output.rect.Height < 3)
return null; // return empty room if no shared wall
if (output.rect.Width>1)
output.rect.Inflate(-1, 0);
if (output.rect.Height>1)
output.rect.Inflate(0, -1);
if (output.rect.Width != 1 && output.rect.Height != 1)
return null; // no shared wall
return output;
}
//
// Determine door width
//
private int getDoorWidth(int max)
{
x = R.r.Next(Config.SINGLE_DOOR_PROB + Config.DOUBLE_DOOR_PROB + Config.HALLWAY_DOOR_PROB);
Debug.WriteLine(max);
if (x < Config.SINGLE_DOOR_PROB)
return 1;
else if (x >= Config.SINGLE_DOOR_PROB && x < Config.SINGLE_DOOR_PROB + Config.DOUBLE_DOOR_PROB)
return Math.Min(2, max);
else
return max>=3?R.r.Next(3, max+1):max;
}
}
//
// Object Class
//
public class Cell
{
public uint id;
public Rectangle rect;
}
//
// Room Class
//
public class Room:Cell
{
public byte type;
public Color mainColor;
public Color accentColor;
public Leaf leaf;
enum RoomType:byte { livingRoom, bedRoom, kitchen, storage, foyer, corridor, bathRoom }
public Room(int x, int y, int width, int height)
{
rect.X = x;
rect.Y = y;
rect.Width = width;
rect.Height = height;
}
public Room(Rectangle r)
{
rect.X = r.X;
rect.Y = r.Y;
rect.Width = r.Width;
rect.Height = r.Height;
}
public Room()
{
rect = new Rectangle();
}
public string asString()
{
return "id="+id+" "+rect;
}
}
//
// Door Class
//
public class Door:Cell
{
public byte type;
public Color mainColor;
public Leaf leaf;
enum DoorType:byte { singleDoor, doubleDoor, hallWay }
}