Cellular Automata Method for Generating Random Cave-Like Levels

From RogueBasin
Revision as of 22:15, 1 April 2023 by Telkoth (talk | contribs) (the old C# sample did not show example usage, and contained bugs, anyway. it also contained needless (& failed) attempts to outsmart the compiler)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Introduction

It is an old and fairly well documented trick to use cellular automata to generate cave-like structures. The basic idea is to fill the first map randomly, then repeatedly create new maps using the 4-5 rule: a tile becomes a wall if it was a wall and 4 or more of its eight neighbors were walls, or if it was not a wall and 5 or more neighbors were. Put more succinctly, a tile is a wall if the 3x3 region centered on it contained at least 5 walls. Each iteration makes each tile more like its neighbors, and the amount of overall "noise" is gradually reduced:

original:           iteration 1:        2:                  3:                  4:
 #       ### ##     ####  ##########    ####  ##########    ####  ##########    ####  ##########
# ##  ## ## #       # #      ##### #    ###      #######    ###      #######    ###      #######
 # # ##   #### #    # #   #   #### #    ##        ######    ##        ######    ##        ######
 # #  #  # ## #     ###  #     ### #    ##         #####    ##         #####    ##         #####
### # #     # #     #    #     #####    ##         #####    ##         #####    ##        ######
    # # ## #####    ##     ###  ####    #       ## #####    ##      ########    ##      ########
##     ####  #      #      #### ####    ##     ####  ###    ##     #### ####    ##     #########
## ## # ## #  ##    ###    ####    #    ###    ###   ###    ###    ####  ###    ###    #########
# ##  ###  #  #     ##### ###    ###    ####  #####  ###    ####  ##### ####    ####   #########
#  # # #  # ###     # ### ##########    ###### #########    #####  #########    ####   #########
  ## ## #### # #    #####    #######    ####    ########    ####    ########    ####    ########
####    # # # #     # #      #######    ##        ######    ##       #######    ##       #######
       #   ## ##    #          #####    #         ######    #         ######    #         ######
   # #    # ####               #####    #         ######    #         ######    #         ######
# # #  ## ######    #        #######    #        #######    ##       #######    ##       #######
#  #  # ####  #     ################    ################    ################    ################

If 45% of the original random map contains walls and the process is repeated 5 times, the output might look like the following:

############################################################
###....####################################.....############
##......######################..#########.........##########
##......#####################....#######...........####.####
##......###################.........................##...###
##......##################..........................###...##
#........##############.............................###...##
#........#############...............................#....##
##.......##############..................................###
##.......###..############..............................####
##.......##....############.............................####
#..............############...###........................###
#...............###########..#####...............##.......##
#................#################...............##.......##
##.....#####..........###########....#..........###.......##
##....#######...........########....###.........####......##
##....#######............######....####........#####......##
##....#######.............####....#####.......#####......###
#......######..............###....####........####......####
#.......######.............###...####.........###.......####
#........#####.............###..####.....................###
##........####..............#...####.....................###
#####......##...................####.....................###
######...........................##.....................####
######..................................................####
######.........###.....................####.............####
######......#########.................######............####
#######....#############.......##############.....###..#####
##############################################..############
############################################################

Advantages

Some advantages include:

  • Procedurally generated so no two levels are (likely) exactly the same.
  • Relatively simple concept, and implementation.
  • A natural, cave-like map to add variety, uniqueness or an alternative to room-based dungeons.


Disadvantages

Some disadvantages include:

  • Generation of isolated cave sections, possibly blocking player advancement (see possible solutions below).
  • Probably not appropriate for generating towns, so other map generators will probably need to be implemented.
  • Very large maps tend to not look as natural, or may require some very fine tuning.

The isolated cave problem

The problem is that the results are inconsistent. The algorithm prone to generating disconnected maps. See example below:

############################################################
#####################################################..#####
#####.....##################...###############............##
#####......###########.####....########.#####..............#
#####.......#########..###.....###############.............#
####.............#####.###....###################.........##
###...............########...####################........###
##.................#######...####################........###
##.......##.........#####.....##################.........###
##......####.........###.......################...........##
##.....########.................#######..######...........##
##...###########................######...#######..........##
#########..######..............######....########........###
########....######..#####......#####.....##########......###
#######......#############.....#####.....###########.....###
#######.......############......###.......###########.....##
#######.......###########.......###.......###########.....##
######.......####..######.......####.......#########......##
#####.......####....#####.......####..........######.....###
####........####......####......####...........#####.....###
####.........###.......###......####...##......######.....##
####...##.....###.......#......###########.....#######.....#
#####.####.....#####...........############....########....#
##########.....######..........############....#########..##
#########.......#####...........##########.....#############
#########.......####...............#####........############
##########......####................###...........##########
###########....#####.....######.....####...........#########
################################...##########.....##########
############################################################

It also sometimes generates maps which consist of one huge open space:

############################################################
############################################################
#################.####.######..######........###############
########...#####...##...####....####..........######...#####
###.####...####....###..####....####..........#####.....####
##...###....##.....###...##.....###............###......####
###.####...........###..........###.....###.........########
########...........###...........#.......#.........#########
#######.............#..............................#########
######.....##..........................................#####
###.......###...........................................####
##.......####...........................................####
##......................................................####
##.....................................................#####
##.....................................................#####
#.....................................##...............#####
#.....................................##................####
#...........................###......###................####
##...........###............###.....#####...............####
###...........#..............##.....######............######
######.......................##......######........#########
##########..................##.........#####......##########
###########................###..........####......##########
############................#...........####......##########
###.....#####.........##...............#####.......####..###
###.....#####.......#####.............#####..............###
###.....####.......#####..............#####.....##.......###
####...#####.......#####.....#####...######....####.....####
#############.##########....################..##############
############################################################

There are many ways to fix the disjoint segments problem. One approach might be to throw away maps that have disjoint segments in them. Another approach would be to connect the segments after the fact. Another still would be to fill in all but the biggest segment. We can't just retry when we get a disjoint map, because if the map is big then, statistically, that will be almost 100% of the time. Filling in all but the biggest segment will tend to produce a small area in a map that was supposed to be big. Connecting up the regions works, but it tends to look unnatural, as in the example from above, now connected:

############################################################
#####################################################..#####
#####.....##################...###############............##
#####......###########.####....########....................#
#####.......#########..###.....###############.............#
####.............####..###....###################.........##
###...............###.####...####################........###
##.................##.####...####################........###
##.......##.........#.###.....##################.........###
##......####..........##.......################...........##
##.....########.................#######..######...........##
##...###########................######...#######..........##
#########..######..............######....########........###
########............#####......#####.....##########......###
#######......#############...............###########.....###
#######.......############......###.......###########.....##
#######.......###########.......###.......###########.....##
######.......####..######.......####.......#########......##
#####.......####....#####.......####.....................###
####........####......####......####...........#####.....###
####.........###.......###......####...##......######.....##
####...##.....###.......#......###########.....#######.....#
#####.####.....#####...........############....########....#
##########.....######..........############....#########..##
#########.......#####...........##########.....#############
#########.......####...............#####........############
##########......####................###...........##########
###########....#####.....######.....####...........#########
################################...##########.....##########
############################################################

Rule Tweaking

The solution to both problems, as it turns out, is to revisit the original cellular automata rules. Recall that the original rule was

  • There is a wall initially at P with 45% probability
  • In the next generation, there is a wall at spot P if the number of tiles around P which are walls is at least 5

Or, in more compact notation:

  • Winit(p) = rand(0,100) < 45
  • R(p) = the number of tiles within 1 step of p which are walls
  • W'(p) = R(p) >= 5

Now, one of the problems was that we tend to get big, open areas. So why not try filling those areas in? Change the rule to

  • W'(p) = R(p) >= 5 or p is in the middle of an open space

Or more formally,

  • Rn(p) = the number of tiles within n steps of p which are walls
  • W'(p) = R1(p)>=5 || R2(p)=0

So how does it look?

Winit(p) = rand(0,100) < 45
Repeat 5: W'(p) = R1(p) >= 5 || R2(p) <= 1
############################################################
##....######################################################
#.......###..#####..####....###########################..###
#...#.........###.............##########..############....##
##...###...#..###..............########....######..###....##
###...######...#..##...........######.....######....###..###
####..######......##..##........####...#..######....########
####..###.#.......#...##...#.....##...###..######...########
#####.........##...........##........####.....##.....#######
#####.........##...........#.......##.....#.............####
####...###...##................#...##......###...........###
####...###..##...###....##.....##..##..##..###....##.....###
#########..###..####...###.............###..##..####.....###
########...##..#####...##...............#...#...####....####
#######........######......###...##....................#####
#######..##.....######....##########...................#####
######..####.......####...#########...................######
####....####..##....##.....#######...##..............#######
###.....###..#####......#...####....##................######
##..##..###..###........##.........#....#......##......#####
#..####..##...##.................###...##.....####......####
#.....#..###..#..##..........#..###..###.....#####......####
##.......####.....#...##........##..###....#######......####
######....###.......................###...#######....#######
#######......................##.....###...#######...########
########.................#######....####...#####....########
########..............###########..######...........########
#########....####....######################........#########
###################.########################################
############################################################

This is more interesting - it doesn't have any big open areas, it has a decent layout. It's almost fully connected. Still, it has some new problems: there are isolated single-tile walls in places, and in general it's not very smooth. But with a little tweaking:

Winit(p) = rand(0,100) < 40
Repeat 4: W'(p) = R1(p) >= 5 || R2(p) <= 2
Repeat 3: W'(p) = R1(p) >= 5
############################################################
###...###########..#############################.....#######
##..........####....################..#########.........####
##...........##.....####..#########.......####..######...###
##.......#..........###....###.................########..###
##......###........###........................#########..###
##.......##.........#........................##########...##
##.......###...........##.............###....#########.....#
##.......######.......####...........#####....#####........#
###.....#########....#####...........######...#####........#
###########################...........#####...#######.....##
#############...###########.............##....########....##
############.........#######...................#######....##
###########...........########......###............##....###
###..#####.............#########...##########............###
##....###...............######################..........####
###..........................######..#########..........####
####..........................###.....#######...........####
####.................##................##................###
####...###..........####...............#..................##
###...#####.........####..............##......##...........#
##....########......#####............####....####..........#
##....#########.....#####............####....####..........#
##.....######.......#####.............##.....####...##.....#
##......##..........#####....................####..####....#
###.................####.........###........############..##
###............##..######.###...############################
####..........##############################################
######..####################################################
############################################################

Notice that the initial fill percentage is a little lower, the cutoffs are different, and we switch rules after a few generations. This is more like the desired result. In the Example Output section, you can see some samples with these parameters, at various sizes.

Flood Fill

This algorithm creates good looking caves, but the problem is isolated caves. The way I solved this is by picking a random, open point on the map and flood filling. Any open point outside the flood filled portion gets turned back into a wall. I then check to see if the flood filled portion of the map is more than some threshold percent of the map. If the map is ~45% open it usually looks alright. If the percent is below this, I just start over.

This turns out to work quite well. On large maps, almost always you will select the largest open connected area, and you won't have to restart the regeneration very often, if at all. On very small maps, this doesn't happen as often, but its also very quick to regenerate because the map is small. It ends up that this method just does the right thing to rectify the non-connected maps naturally! -Jlund3

Horizontal Blanking

Another approach to fixing the problem of disconnected areas involves blanking a horizontal strip of walls, about 3 or 4 block tall, in the middle of of the map after the random fill, but before beginning the automata iterations. Depending on the rules, a horizontal strip of sufficient width will prevent a continuous vertical wall from forming, creating disconnected cave sections.

Example Output

Animated GIF of the cellular automata cave process with 12 smoothing iterations (Generated using haskell): cavegen.gif

##############################
##..###....###.....###########
#...........................##
#.................##.........#
##..###..........#######.....#
########.........#######.....#
############......####......##
#############...............##
###..########...............##
##....#######...###..#......##
##.....######...#######.....##
##......#####...######......##
##...#...###....#####.......##
##..##.........######.......##
##..........##########......##
###........#####..####.....###
###........####....###.....###
###....##...##.....###.....###
###...####........####......##
###...#####......#####......##
##....#####.....#####.......##
##.....###.....####.........##
##..............##..........##
###.........................##
####........................##
####.......................###
####..........##...........###
#####...###..####.........####
####################.....#####
##############################
############################################################
#########......###...##........####...####.....#############
####............................##.....##............#######
###................###.....##..........................#####
###...............#####...####..........................####
###...............#####....####............#............####
####...###.........####.......##.......................#####
##########..........###........##.....................######
##########...........#..........##...................#######
#########.......................#####..............#########
######.........................#######...#......############
#####..........................############....#############
####...........................###########......######..####
###..........##.................#########................###
##.......#######........#..........######...###.........####
##......########.......##............###...######.....######
###.....#######...............#####........########..#######
###......#####...##...........######........################
###......#####..####...........#####.........###############
#######..#####..####............###...........#######....###
########..###...#####......###.................#####......##
########.......######......####.................###.......##
########.......######.......##....##..................##..##
#######.......######....##.......####................####..#
######.......#######....###......####..........###..#####..#
#####........######.....######....##..........##########...#
######........###........######...............########.....#
##########...............#######.............########.....##
#############...#######..###########...#####################
############################################################
################################################################
#######..#####............####..####...###....###########...####
###......####..................................#########.....###
##.......###.......................................####......###
##......####.............##.........................##......####
##......#####......##...####................##..............####
##......##############..#####...............###.............####
##.....###############...#####...............##........##...####
##.....################..######.........#.............####...###
##.....################...#####........###............####....##
###.....#####..##...##....####.........###.............###....##
####....####..............###..........###...................###
####....####.............####..........##....................###
###.....####...........#####.........................##.....####
##.......##............#####....................##..####...#####
##.....................######................###########...#####
###..##........##......###############.....############....#####
########......####....#################..###############..######
#########...####################################################
################################################################ 
############################################################
#########......###...##........####...####.....#############
####............................##.....##............#######
###................###.....##..........................#####
###...............#####...####..........................####
###...............#####....####............#............####
####...###.........####.......##.......................#####
##########..........###........##.....................######
##########...........#..........##...................#######
#########.......................#####..............#########
######.........................#######...#......############
#####..........................############....#############
####...........................###########......######..####
###..........##.................#########................###
##.......#######........#..........######...###.........####
##......########.......##............###...######.....######
###.....#######...............#####........########..#######
###......#####...##...........######........################
###......#####..####...........#####.........###############
#######..#####..####............###...........#######....###
########..###...#####......###.................#####......##
########.......######......####.................###.......##
########.......######.......##....##..................##..##
#######.......######....##.......####.........##.....####..#
#####........#######....###......####........#####..#####..#
####........#######.....######...#####.......############..#
####.......######..........####..#########..#############..#
###........#####............###..########################.##
##.........####.............###..################.######..##
#....###...####......####....#...######..#######...#####..##
#.....#.....##......######.......#####....#####....#####..##
#...................######........####....###.....#####...##
#....................#####........#####..###......##......##
#....................######........########................#
##......#............########.......#######................#
##......##...........#########......#######...............##
###.....#............####...##.....#######..##...........###
###..........#......####...........######..####..........###
##..........####....####...........#####..######.........###
##...........####..#####............##....######.........###
##............###..######......#...........####..........###
##............###..#######.....##........................###
##.......###...#....#######....#..........................##
###......###.........######.........................##.....#
###.......#..........######........#####...........####....#
###............###...######......########.........#####....#
###...........#####..######.....#########.........####.....#
###...........#####.#######.....########...........###.....#
###...........####..########...#########......##...###....##
###...........####...##################......####..###....##
###...........####......##############.......####..###....##
###...........####..........##########........##...###....##
###............####..........#########.............####..###
###...........#####...........#######..............#########
###.....##########............######.......##......#########
##.....###########.....###.....####.......####......########
##.....############....###......##.......#####........######
###...##############..#####.............#######.......######
################################...##..#####################
############################################################


Example Code

C Code

Original article by Jim Babcock

 #include <stdio.h>
 #include <stdlib.h>
 #include <time.h>

 #define TILE_FLOOR 0
 #define TILE_WALL 1

 typedef struct {
 	int r1_cutoff, r2_cutoff;
 	int reps;
 } generation_params; 
 
 int **grid;
 int **grid2; 
 
 int fillprob = 40;
 int r1_cutoff = 5, r2_cutoff = 2;
 int size_x = 64, size_y = 20;
 generation_params *params;  

 generation_params *params_set;
 int generations;

 int randpick(void)
 {
 	if(rand()%100 < fillprob)
 		return TILE_WALL;
 	else
 		return TILE_FLOOR;
 }

 void initmap(void)
 {
	int xi, yi;
	
	grid  = (int**)malloc(sizeof(int*) * size_y);
	grid2 = (int**)malloc(sizeof(int*) * size_y);
	
	for(yi=0; yi<size_y; yi++)
	{
		grid [yi] = (int*)malloc(sizeof(int) * size_x);
		grid2[yi] = (int*)malloc(sizeof(int) * size_x);
	}
	
	for(yi=1; yi<size_y-1; yi++)
	for(xi=1; xi<size_x-1; xi++)
		grid[yi][xi] = randpick();
	
	for(yi=0; yi<size_y; yi++)
	for(xi=0; xi<size_x; xi++)
		grid2[yi][xi] = TILE_WALL;
	
	for(yi=0; yi<size_y; yi++)
		grid[yi][0] = grid[yi][size_x-1] = TILE_WALL;
	for(xi=0; xi<size_x; xi++)
		grid[0][xi] = grid[size_y-1][xi] = TILE_WALL;
 }

 void generation(void)
 {
	int xi, yi, ii, jj;
	
	for(yi=1; yi<size_y-1; yi++)
	for(xi=1; xi<size_x-1; xi++)
 	{
 		int adjcount_r1 = 0,
 		    adjcount_r2 = 0;
 		
 		for(ii=-1; ii<=1; ii++)
		for(jj=-1; jj<=1; jj++)
 		{
 			if(grid[yi+ii][xi+jj] != TILE_FLOOR)
 				adjcount_r1++;
 		}
 		for(ii=yi-2; ii<=yi+2; ii++)
 		for(jj=xi-2; jj<=xi+2; jj++)
 		{
 			if(abs(ii-yi)==2 && abs(jj-xi)==2)
 				continue;
 			if(ii<0 || jj<0 || ii>=size_y || jj>=size_x)
 				continue;
 			if(grid[ii][jj] != TILE_FLOOR)
 				adjcount_r2++;
 		}
 		if(adjcount_r1 >= params->r1_cutoff || adjcount_r2 <= params->r2_cutoff)
 			grid2[yi][xi] = TILE_WALL;
 		else
 			grid2[yi][xi] = TILE_FLOOR;
 	}
 	for(yi=1; yi<size_y-1; yi++)
 	for(xi=1; xi<size_x-1; xi++)
 		grid[yi][xi] = grid2[yi][xi];
 } 

 void printfunc(void)
 {
 	int ii;
 	
 	printf("W[0](p) = rand[0,100) < %i\n", fillprob);
 	
 	for(ii=0; ii<generations; ii++)
 	{
 		printf("Repeat %i: W'(p) = R[1](p) >= %i",
 			params_set[ii].reps, params_set[ii].r1_cutoff);
 		
 		if(params_set[ii].r2_cutoff >= 0)
 			printf(" || R[2](p) <= %i\n", params_set[ii].r2_cutoff);
 		else
 			putchar('\n');
 	}
 }
 
 void printmap(void)
 {
 	int xi, yi;
 	
 	for(yi=0; yi<size_y; yi++)
 	{
 		for(xi=0; xi<size_x; xi++)
 		{
 			switch(grid[yi][xi]) {
 				case TILE_WALL:  putchar('#'); break;
 				case TILE_FLOOR: putchar('.'); break;
 			}
 		}
 		putchar('\n');
 	}
 }

 int main(int argc, char **argv)
 {
 	int ii, jj;
 	
 	if(argc < 7) {
 		printf("Usage: %s xsize ysize fill (r1 r2 count)+\n", argv[0]);
 		return 1;
 	}
 	size_x     = atoi(argv[1]);
 	size_y     = atoi(argv[2]);
 	fillprob   = atoi(argv[3]);
 	
 	generations = (argc-4)/3;
 	
 	params = params_set = (generation_params*)malloc( sizeof(generation_params) * generations );
 	
 	for(ii=4; ii+2<argc; ii+=3)
 	{
 		params->r1_cutoff  = atoi(argv[ii]);
 		params->r2_cutoff  = atoi(argv[ii+1]);
 		params->reps = atoi(argv[ii+2]);
 		params++;
 	}
 	
 	srand(time(NULL));
 	
 	initmap();
 	
 	for(ii=0; ii<generations; ii++)
 	{
 		params = &params_set[ii];
 		for(jj=0; jj<params->reps; jj++)
 			generation();
 	}
 	printfunc();
 	printmap();
 	return 0;
 }


C# Code

public static class CellularAutomata
{
    public static bool[] Generate(int width, int height, int iterations = 4, int percentAreWalls = 40)
    {
        var map = new bool[width * height];

        RandomFill(map, width, height, percentAreWalls);
        
        for(var i = 0; i < iterations; i++)
            map = Step(map, width, height);

        return map;
    }
    
    private static void RandomFill(bool[] map, int width, int height, int percentAreWalls = 40)
    {
        var randomColumn = Random.Shared.Next(4, width - 4);
        
        for(int y = 0; y < height; y++)
        {
            for(int x = 0; x < width; x++)
            {
                if(x == 0 || y == 0 || x == width - 1 || y == height - 1)
                    map[x + y * width] = true;
                else if(x != randomColumn && Random.Shared.Next(100) < percentAreWalls)
                    map[x + y * width] = true;
            }
        }
    }

    private static bool[] Step(bool[] map, int width, int height)
    {
        var newMap = new bool[width * height];
        
        for(int y = 0; y < height; y++)
        {
            for(int x = 0; x < width; x++)
            {
                if(x == 0 || y == 0 || x == width - 1 || y == height - 1)
                    newMap[x + y * width] = true;
                else
                    newMap[x + y * width] = PlaceWallLogic(map, width, height, x, y);
            }
        }

        return newMap;
    }

    private static bool PlaceWallLogic(bool[] map, int width, int height, int x, int y) =>
        CountAdjacentWalls(map, width, height, x, y) >= 5 ||
        CountNearbyWalls(map, width, height, x, y) <= 2;

    private static int CountAdjacentWalls(bool[] map, int width, int height, int x, int y)
    {
        var walls = 0;
        
        for(var mapX = x - 1; mapX <= x + 1; mapX++)
        {
            for(var mapY = y - 1; mapY <= y + 1; mapY++)
            {
                if(map[mapX + mapY * width])
                    walls++;
            }
        }

        return walls;
    }
    
    private static int CountNearbyWalls(bool[] map, int width, int height, int x, int y)
    {
        var walls = 0;
        
        for(var mapX = x - 2; mapX <= x + 2; mapX++)
        {
            for(var mapY = y - 2; mapY <= y + 2; mapY++)
            {
                if(Math.Abs(mapX - x) == 2 && Math.Abs(mapY - y) == 2)
                    continue;
                
                if(mapX < 0 || mapY < 0 || mapX >= width || mapY >= height)
                    continue;
                        
                if(map[mapX + mapY * width])
                    walls++;
            }
        }

        return walls;
    }
}

Example usage:

var width = 30;
var height = 20;
var tiles = new char[width * height];

var walls = CellularAutomata.Generate(width, height);
        
for(int y = 0; y < height; y++)
{
    for(int x = 0; x < width; x++)
    {
        tiles[x + y * width] = walls[x + y * width] ? '#' : '.';
    }
}

Implementations