Daily Archives: 7 July, 2014

Postprocessing Code to Fully Connect Dungeon Maps

Some map generation algorithms generate connected maps automatically, because they always work by extending a connected square.

But other algorithms, including cellular automata which produce some of the most natural-looking caves, don’t.

Depending on your map generator, you may need a post-processing step to make sure that your map is fully connected. I wanted one that would not change the outline and contour of any of the generated interiors, so I wrote this.

Here are a couple bits of code that solve the problem, completely. This code is fully general; it will work on any kind of map.

It works by producing successive maps in which the unconnected spaces are brought closer to the top, left corner for as long as it is possible for them to move upward or left. Then, if there are still any unconnected areas, it flips the map around the diagonal that runs from the lower-left to upper-right, and repeats the process. When it terminates, it is guaranteed that all open spaces in the map are joined into a single walkable space.

As an example, here’s the output of a cellular automaton that generates caves. They’re nice looking caves, but in this case they’re not all connected.

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

Now here they are after running

  do joinall(map1,map2); while (joinall(map2,map1) > 1);

What happened was that every unconnected region that could be shifted up or left or both shifted up or left or both until it ran into another region – and the region it ran into, which wasn’t moving in the same direction it was, was one that couldn’t be shifted any further up or left or both because it was already in contact with the upper or left edge.

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

Ta-dah! Just shifting up-and-left until collision occurs works to join most maps this size.

But that technique just used by itself, can fail. Here’s an example of why. Given the initial cave level:

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

We start in. While there are still separate regions, and we can shift any of them up and left, we do that. We get a lot of collisions joining regions. But we wind up not having everything joined, because the little region in the upper left corner never contacted anything and the other region can’t move that direction because it’s touching both the left wall below the unjoined region, and the top to the right of the unjoined region.

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

So the next iteration of joinall detects that there are still multiple floodfill regions but none of them can move up and/or left, so it flips the whole map around the diagonal, like this. Now the little region at the upper left has become the little region at the lower right.

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

Two more iterations brings the big joined region up against the top and left edge, as before but now upside down. Four more after that brings the little freefloating region from the lower right corner into contact with it.

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

And now this map is fully joined.

The key realization is that separation which endures the first iteration set can only happen when the region closer to the corner *can* move in the opposite direction until it contacts another region. So flipping around the diagonal solves the problem, and we get an algorithm that works on any map. Computationally it’s a brute-force solution because it floodfills the open areas of the map on every iteration. But it’s reasonably straightforward and both correct results and finite-time termination are guaranteed.

here is the code:

/* constants: XMAX and YMAX are the size of your map, and
   REGION is some number of regions you want to be able to
   operate on simultaneously.  If REGION is smaller than the
   actual number of disconnected spaces on your map, this 
   is still guaranteed to join all spaces and terminate, 
   but will take significantly more iterations and is likely
   to produce different results. 
*/

/* floodfills one walkable area of a map, starting at point x,y 
   and writing mark into every walkable tile of that set. 
   simultaneously updates minx and miny to that mark wherever 
   needed. */

void floodfill(int map[XMAX][YMAX], int x, int y, int mark,
               int minx[REGION], int miny[REGION]){
  int i;
  int j;

  for (i=-1;i<=1;i++)
    for (j=-1;j<=1;j++)
      if (i+x < XMAX && i+x >= 0 &&
          j+y < YMAX && j+y >= 0 &&
          map[i+x][j+y] != 0 && map[i+x][j+y] != mark){
        map[i+x][j+y] = mark;
        /* side effect of floodfilling is recording minimum x and y
           for each region*/
        if (mark < REGION){
          if (i+x < minx[mark]) minx[mark] = i+x;
          if (j+y < miny[mark]) miny[mark] = j+y;
        }
        floodfill(map, i+x, j+y, mark, minx, miny);
      }
}


/* finds all regions, mark each open cell (by floodfill) with an integer
   2 or greater indicating what region it's in. */

int floodall(int map[XMAX][YMAX], int minx[REGION], int miny[REGION]){
  int x;
  int y;
  int count = 2;
  int retval = 0;

  /* start by setting all floor tiles to 1. */
  /* wall spaces are marked 0. */
  for (x=0;x< XMAX;x++){
    for (y=0;y< YMAX;y++){
      if (map[x][y] != 0){
        map[x][y] = 1;
      }
    }
  }

  /* reset region extent marks to -1 invalid */
  for (x=0;x<REGION;x++){
    minx[x] = -1;
    miny[x] = -1;
  }

  /* now mark regions starting with the number 2. */
  for (x=0;x< XMAX;x++){
    for (y=0;y< YMAX;y++){
      if (map[x][y] == 1){
        if (count < REGION){
          maxx[count] = x;
          minx[count] = x;
          maxy[count] = y;
          miny[count] = y;
        }
        floodfill(map, x, y, count, maxx, minx, maxy, miny);
        count++;
        retval++;
      }
    }
  }
  /* return the number of floodfill regions found */
  return(retval);
}

/* joinall makes an iterative step toward joining all map regions.
   The output map is guaranteed to have the same number of
   open spaces, or fewer, than the input.  With enough
   iterations, an output map having exactly one open space
   will be produced. Further iterations will just copy that
   map.  Call using an idiom like 

   do {
      joinall(map1, map2);
      regioncount = joinall(map2, map1);
   } while (regioncount > 1)

   to get a fully connected map.   

  Algorithm/Data:
   Joinall uses the minx and miny arrays to record the 
   minimum x and y coordinates of squares found in each 
   region.  This is how it determines which regions can 
   be shifted toward [0,0].  If any regions can be 
   shifted, it produces an output map by shifting those
   regions.  If none can be shifted, it produces an output
   map by flipping the input map around the diagonal.  If
   there was only one region in the first place, it just
   copies the input map to the output map.  It returns the
   number of unjoined regions found in the input map. 

   The maps are arrays of integers; each open tile is marked
   with a number 2 or greater indicating what region it 
   belongs to.  Wall tiles are marked 0, and 1 is used 
   internally for unmarked open tiles.
*/
int joinall(int mapin[XMAX][YMAX], int mapout[XMAX][YMAX]){

  int minx[REGION];
  int miny[REGION];
  int x;
  int y;
  int count;
  int retval;

  retval = floodall(mapin, minx, miny);

  /* if we have multiple unconnected regions */    
  if (retval > 1){
   
    /* check to see if there are still regions that can be moved toward
       0,0. */
    count = 0;
    for (x = 2; x < REGION; x++)
      if (minx[x] > 0 || miny[x] > 0) count = 1;


    /* if no regions can still be moved toward (0,0) */
    if (count == 0){
      /* the new map is the old map flipped about the diagonal. */
      for (x = 0; x < XMAX; x++)
        for (y = 0; y < YMAX; y++)
          mapout[XMAX-x-1][YMAX-y-1] = mapin[x][y];
      return(retval);
    }

    else{ /* there exist regions that can be moved toward 0,0 */
      /* write rock to every square of new map that is either
         rock or a region we can move; copy the map to all other squares. */
      for (x = 0; x < XMAX; x++)
        for (y = 0; y < YMAX; y++){
          if (mapin[x][y] >= REGION) mapout[x][y] = mapin[x][y];
          else mapout[x][y] = 0;
        }
   
      /* now copy regions we can move to new map, each with a shift
         toward (0,0).  Ugh.  This is ugly code.  Remember that in C 
         comparison results are integer, not boolean.  So this is a 
         triply nested array reference using them as integers, which 
         won't work in most sane languages.  If you're translating
         this into a sane language, TRUE is 1 and FALSE is 0.   */
      for (x = 0; x < XMAX; x++)
        for (y = 0; y < YMAX; y++)
          if (mapin[x][y] != 0 && mapin[x][y] < REGION){
            mapout[x-(minx[mapin[x][y]] > 0)]
                  [y-( miny[mapin[x][y]] > 0)] = 1;
          }
      return(retval);  
    }
  }
  else{ /* there was only one connected region - the output map is the
           same as the input map.  */
    for (x = 0; x < XMAX; x++)
      for (y = 0; y < YMAX; y++)
        if (mapin[x][y] == 0) mapout[x][y] = 0;
        else mapout[x][y] = 1;
    return(1);
  }
}