# A Simple Dungeon Generator for Python 2 or 3

m |
|||

Line 18: | Line 18: | ||

At it's heart this dungeon generator is a simple beast, it merely tries to randomly place rooms, optionally removing rooms that overlap. It then joins the rooms with corridors. | At it's heart this dungeon generator is a simple beast, it merely tries to randomly place rooms, optionally removing rooms that overlap. It then joins the rooms with corridors. | ||

− | What is nice about this generator is that it maintains a list of rooms and corridors. To add doors the rooms all you have to do is walk over the list of rooms, look at the edges of the rooms, and decorate them with doors (which is what I do in the descendants of this generator). Another useful function would be to walk over the corridor list removing corridors that only connect rooms that other corridors | + | What is nice about this generator is that it maintains a list of rooms and corridors. To add doors the rooms all you have to do is walk over the list of rooms, look at the edges of the rooms, and decorate them with doors (which is what I do in the descendants of this generator). Another useful function would be to walk over the corridor list removing corridors that only connect rooms that other corridors already have connected (also something I did in the descendants of this generator). That helps prevent rooms from looking too chopped up with corridors. Since the corridor list is generated by walking over the room list and joining one room to the next you can get dungeons of very different character by modifying the order of the room list (eg, by sorting the room list in order of the next nearest room), then re-generating the corridors. By maintaining a corridor and room list it makes it trivial to go back after you generate a dungeon and decorate and populate rooms and corridors. |

## Revision as of 13:49, 12 April 2016

**by James Spencer**

**Notes on the Generator:**

This is a descendant of my first dungeon generator. It's GPL 3, and you should be able to save it to a file and just run it by typing: python <filename.py> on the console. It should be easy to integrate into any project.

This Dungeon Generator allows you to set:

- The width of the area (width=64)
- The height of the area (height=64)
- The maximum number of rooms (max_rooms=15)
- The minimum room width / height (min_room_xy=5)
- The maximum room width / height (max_room_xy=10)
- If the rooms can overlap (rooms_overlap=False)
- The number of randomly connected rooms (random_connections=1)
- The number of random spurs from a room to a random point (random_spurs=3)

At it's heart this dungeon generator is a simple beast, it merely tries to randomly place rooms, optionally removing rooms that overlap. It then joins the rooms with corridors.

What is nice about this generator is that it maintains a list of rooms and corridors. To add doors the rooms all you have to do is walk over the list of rooms, look at the edges of the rooms, and decorate them with doors (which is what I do in the descendants of this generator). Another useful function would be to walk over the corridor list removing corridors that only connect rooms that other corridors already have connected (also something I did in the descendants of this generator). That helps prevent rooms from looking too chopped up with corridors. Since the corridor list is generated by walking over the room list and joining one room to the next you can get dungeons of very different character by modifying the order of the room list (eg, by sorting the room list in order of the next nearest room), then re-generating the corridors. By maintaining a corridor and room list it makes it trivial to go back after you generate a dungeon and decorate and populate rooms and corridors.

**The Python source code follows:**

#! /usr/bin/env python # coding: utf-8 # generator-1.py, a simple python dungeon generator. # Copyright (C) 2016 James Spencer <jamessp [at] gmail.com> # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # To receive a copy of the GNU General Public License write to the # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, # Boston, MA 02110-1301 USA from __future__ import print_function import random CHARACTER_TILES = {0: ' ', 1: '.', 2: '#'} class Generator(): def __init__(self, width=64, height=64, max_rooms=15, min_room_xy=5, max_room_xy=10, rooms_overlap=False, random_connections=1, random_spurs=3, tiles=CHARACTER_TILES): self.width = width self.height = height self.max_rooms = max_rooms self.min_room_xy = min_room_xy self.max_room_xy = max_room_xy self.rooms_overlap = rooms_overlap self.random_connections = random_connections self.random_spurs = random_spurs self.tiles = CHARACTER_TILES self.level = [] self.room_list = [] self.corridor_list = [] self.tiles_level = [] def gen_room(self): x, y, w, h = 0, 0, 0, 0 w = random.randint(self.min_room_xy, self.max_room_xy) h = random.randint(self.min_room_xy, self.max_room_xy) x = random.randint(1, (self.width - w - 1)) y = random.randint(1, (self.height - h - 1)) return [x, y, w, h] def room_overlapping(self, room, room_list): x = room[0] y = room[1] w = room[2] h = room[3] for room_num, room in enumerate(room_list): # The rectangles don't overlap if # one rectangle's minimum in some dimension # is greater than the other's maximum in # that dimension. if (x > (room[0] + room[2]) or room[0] > (x + w) or y > (room[1] + room[3]) or room[1] > (y + h)): return False else: return True def join_rooms(self, room_1, room_2): x_overlap = False y_overlap = False x1 = room_1[0] y1 = room_1[1] w1 = room_1[2] h1 = room_1[3] x2 = room_2[0] y2 = room_2[1] w2 = room_2[2] h2 = room_2[3] if (((x1 >= x2) and x1 <= (x2 + w2 - 1)) or ((x1 + w1 - 1) >= x2) and ((x1 + w1 - 1) <= (x2 + w2 - 1))): x_overlap = True if (((y1 >= y2) and y1 <= (y2 + h2 - 1)) or ((y1 + h1 - 1) >= y2) and ((y1 + h1 - 1) <= (y2 + h2 - 1))): y_overlap = True if x_overlap: tmp_x_coords = [x1, x1 + w1 - 1, x2, x2 + w2 - 1] tmp_x_coords.sort() if tmp_x_coords[1:2].pop() == tmp_x_coords[2:3].pop(): rand_x = tmp_x_coords[1:2].pop() else: rand_x = random.randint(tmp_x_coords[1:2].pop(), ( tmp_x_coords[2:3].pop())) if y2 >= y1 + h1 - 1: tmp_y = y1 + h1 tmp_h = y2 - (y1 + h1) else: tmp_y = y2 + h2 tmp_h = y1 - (y2 + h2) self.corridor_list.append([rand_x, tmp_y, 1, tmp_h]) elif y_overlap: tmp_y_coords = [y1, y1 + h1 - 1, y2, y2 + h2 - 1] tmp_y_coords.sort() if tmp_y_coords[1:2].pop() == tmp_y_coords[2:3].pop(): rand_y = tmp_y_coords[1:2].pop() else: rand_y = random.randint(tmp_y_coords[1:2].pop(), ( tmp_y_coords[2:3].pop())) if x2 >= x1 + w1: tmp_x = x1 + w1 tmp_w = x2 - (x1 + w1) else: tmp_x = x2 + w2 tmp_w = x1 - (x2 + w2) self.corridor_list.append([tmp_x, rand_y, tmp_w, 1]) else: sortable = [room_1, room_2] sortable.sort() left_bottom = False start_on_x = False if sortable[0][1] > sortable[1][1]: left_bottom = True if random.randint(0, 1) == 1: start_on_x = True if left_bottom: if start_on_x: x1 = (sortable[0][0] + sortable[0][2] - 1) + 1 y1 = random.randint(sortable[0][1], ( sortable[0][1] + sortable[0][3] - 1)) tmp = random.randint(sortable[1][0], ( sortable[1][0] + sortable[1][2] - 1)) w1 = (tmp - x1) + 1 h1 = 1 x2 = tmp y2 = (sortable[1][1] + sortable[1][3] - 1) + 1 w2 = 1 h2 = y1 - y2 else: x1 = random.randint(sortable[0][0], ( sortable[0][0] + sortable[0][2] - 1)) y1 = random.randint(sortable[1][1], ( sortable[1][1] + sortable[1][3] - 1)) w1 = 1 h1 = sortable[0][1] - y1 x2 = x1 y2 = y1 w2 = sortable[1][0] - x1 h2 = 1 self.corridor_list.append([x1, y1, w1, h1]) self.corridor_list.append([x2, y2, w2, h2]) else: if start_on_x: x1 = (sortable[0][0] + sortable[0][2] - 1) + 1 y1 = random.randint(sortable[0][1], ( sortable[0][1] + sortable[0][3] - 1)) tmp = random.randint(sortable[1][0], ( sortable[1][0] + sortable[1][2] - 1)) w1 = tmp - x1 h1 = 1 x2 = tmp y2 = y1 w2 = 1 h2 = sortable[1][1] - y1 else: x1 = random.randint( sortable[0][0], (sortable[0][0] + sortable[0][2] - 1)) y1 = (sortable[0][1] + sortable[0][3] - 1) + 1 w1 = 1 tmp = random.randint(sortable[1][1], ( sortable[1][1] + sortable[1][3] - 1)) h1 = tmp - (sortable[0][1] + sortable[0][3] - 1) - 1 x2 = x1 y2 = y1 + h1 w2 = sortable[1][0] - x2 h2 = 1 self.corridor_list.append([x1, y1, w1, h1]) self.corridor_list.append([x2, y2, w2, h2]) def gen_level(self): # build an empty dungeon, blank the room and corridor lists for i in range(self.height): self.level.append([0] * self.width) self.room_list = [] self.corridor_list = [] max_iters = self.max_rooms * 5 for a in range(max_iters): tmp_room = self.gen_room() if self.rooms_overlap or not self.room_list: self.room_list.append(tmp_room) else: tmp_room = self.gen_room() tmp_room_list = self.room_list[:] if self.room_overlapping(tmp_room, tmp_room_list) is False: self.room_list.append(tmp_room) if len(self.room_list) >= self.max_rooms: break # connect the rooms for a in range(len(self.room_list) - 1): self.join_rooms(self.room_list[a], self.room_list[a + 1]) # do the random joins for a in range(self.random_connections): room_1 = self.room_list[random.randint(0, len(self.room_list) - 1)] room_2 = self.room_list[random.randint(0, len(self.room_list) - 1)] self.join_rooms(room_1, room_2) # do the spurs for a in range(self.random_spurs): room_1 = [random.randint(2, self.width - 2), random.randint( 2, self.height - 2), 1, 1] room_2 = self.room_list[random.randint(0, len(self.room_list) - 1)] self.join_rooms(room_1, room_2) # fill the map # paint rooms for room_num, room in enumerate(self.room_list): for b in range(room[2]): for c in range(room[3]): self.level[room[1] + c][room[0] + b] = 1 # paint corridors for corr_num, corridor in enumerate(self.corridor_list): for b in range(corridor[2]): for c in range(corridor[3]): self.level[corridor[1] + c][corridor[0] + b] = 1 # paint the walls for a in range(1, len(self.level) - 1): for b in range(1, len(self.level[a]) - 1): if self.level[a][b] == 1: if self.level[a - 1][b - 1] == 0: self.level[a - 1][b - 1] = 2 if self.level[a - 1][b] == 0: self.level[a - 1][b] = 2 if self.level[a - 1][b + 1] == 0: self.level[a - 1][b + 1] = 2 if self.level[a][b - 1] == 0: self.level[a][b - 1] = 2 if self.level[a][b + 1] == 0: self.level[a][b + 1] = 2 if self.level[a + 1][b - 1] == 0: self.level[a + 1][b - 1] = 2 if self.level[a + 1][b] == 0: self.level[a + 1][b] = 2 if self.level[a + 1][b + 1] == 0: self.level[a + 1][b + 1] = 2 def gen_tiles_level(self): for row_num, row in enumerate(self.level): tmp_tiles = [] for col_num, col in enumerate(row): if col == 0: tmp_tiles.append(self.tiles[0]) if col == 1: tmp_tiles.append(self.tiles[1]) if col == 2: tmp_tiles.append(self.tiles[2]) self.tiles_level.append(''.join(tmp_tiles)) if __name__ == '__main__': gen = Generator() gen.gen_level() gen.gen_tiles_level() print('Room List: ', gen.room_list) print('Corridor List: ', gen.corridor_list) for i in range(gen.height): print(gen.tiles_level[i])

**The output should look something like:**

python3 ./generator-1.py Room List: [[4, 20, 8, 9], [25, 17, 10, 9], [53, 17, 5, 9], [16, 35, 9, 10], [27, 4, 5, 9], [18, 33, 8, 10], [16, 52, 10, 6], [53, 33, 5, 6], [32, 1, 9, 6], [40, 40, 8, 6], [47, 55, 10, 6], [52, 18, 5, 7], [57, 32, 6, 6], [47, 33, 5, 10], [41, 16, 7, 10]] Corridor List: [[12, 22, 13, 1], [35, 24, 18, 1], [24, 20, 1, 15], [24, 20, 29, 1], [21, 10, 1, 25], [21, 10, 6, 1], [26, 36, 4, 1], [29, 13, 1, 23], [21, 43, 1, 9], [16, 33, 1, 19], [16, 33, 37, 1], [41, 5, 16, 1], [57, 5, 1, 28], [40, 7, 1, 33], [47, 46, 1, 9], [53, 25, 1, 30], [57, 21, 1, 1], [58, 21, 1, 11], [52, 36, 5, 1], [47, 26, 1, 7], [42, 26, 1, 14], [33, 15, 15, 1], [48, 15, 1, 18], [3, 23, 46, 1], [49, 23, 1, 32], [16, 19, 3, 1], [19, 19, 1, 14]] ########### #.........# #.........# ######.........# #..............################## #...............................# #..............################.# #.....########.# #.# #.....# #.# #.# #######.....# #.# #.# #...........# #.# #.# #.#####.....# #.# #.# #.# #.....# #.# #.# #.# ###.### #.# #.# #.# #.# ########.######### #.# #.# #.# #................# #.# #.# #####.##########.........# #####.# #.# #..........# #.........# ##.....# ######.# #..........# #.........# #......# ########## #....###..........#####.........###......# #........# ####.#.##..................................## #........#######.#.##...........#####.........###.......# ##...............................#####.........###.......# #...............................................##.......# ##........#######.#.##...................................# #........# #.#.##...........#####..........###......# #........# #.#.##.####.###### #.#.####...# #.###..# #........# #.#.##.# #.# #.#.# #...# #.# #..# #........# #.#.##.# #.# #.#.# #...# #.# #..# ########## #.#.##.# #.# #.#.# #...# #.# #..# #.#.##.# #.# #.#.# #...# #.# #..# #.#.##.# #.# #.#.# #...# #.# #..##### ####.#.##.####.##########.#.####...###.###......# #...............................................# #.#........###.##########.#.####.....#..........# #..........###.# #.#.# #.....#..........# #..............# #.#.# #................# #..........##### #.#.# #.....#..........# #..........# #.#.# #.....#.....###### #..........# #.#.####.....#.##### #..........# #............#.# #..........# #............#.# #..........# #............#.# #.........## #........#.###.# #.........# #........#.# #.# #.####.#### #........#.# #.# #.# #.# ########.#.# #.# #.# #.# #.#.# #.# #.# #.# #.#.# #.# #.# #.# #.#.# #.# #.# #.# #.#.# #.# #.####.##### #.#.# #.# #..........# #.#.# #.# #..........# #.#.# #.# #..........# #.#.###.#### #..........# #..........# #..........# #..........# #..........# #..........# ############ #..........# #..........# #..........# ############

I'm still using a distant descendant of this generator for my as of yet unreleased roguelike. I hope others find it useful.