A Simple Dungeon Generator for Python 2 or 3

From RogueBasin
Jump to: navigation, search

by James Spencer


Notes on the Generator:

This is a descendant of my first dungeon generator. Additionally, it is licensed under CC0, 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 to 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.


Pastebin Link to the Python Source Code:

Note: The Pastebin link makes it easy to download the code, and has rather better code highlighting.

http://pastebin.com/zehq4vMM


The Full Python Source Code Follows:

  1. #! /usr/bin/env python
  2. # coding: utf-8
  3.  
  4. # generator-1.py, a simple python dungeon generator by
  5. # James Spencer <jamessp [at] gmail.com>.
  6.  
  7. # To the extent possible under law, the person who associated CC0 with
  8. # pathfinder.py has waived all copyright and related or neighboring rights
  9. # to pathfinder.py.
  10.  
  11. # You should have received a copy of the CC0 legalcode along with this
  12. # work. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
  13.  
  14. from __future__ import print_function
  15. import random
  16.  
  17. CHARACTER_TILES = {'stone': ' ',
  18.                    'floor': '.',
  19.                    'wall': '#'}
  20.  
  21.  
  22. class Generator():
  23.     def __init__(self, width=64, height=64, max_rooms=15, min_room_xy=5,
  24.                  max_room_xy=10, rooms_overlap=False, random_connections=1,
  25.                  random_spurs=3, tiles=CHARACTER_TILES):
  26.         self.width = width
  27.         self.height = height
  28.         self.max_rooms = max_rooms
  29.         self.min_room_xy = min_room_xy
  30.         self.max_room_xy = max_room_xy
  31.         self.rooms_overlap = rooms_overlap
  32.         self.random_connections = random_connections
  33.         self.random_spurs = random_spurs
  34.         self.tiles = CHARACTER_TILES
  35.         self.level = []
  36.         self.room_list = []
  37.         self.corridor_list = []
  38.         self.tiles_level = []
  39.  
  40.     def gen_room(self):
  41.         x, y, w, h = 0, 0, 0, 0
  42.  
  43.         w = random.randint(self.min_room_xy, self.max_room_xy)
  44.         h = random.randint(self.min_room_xy, self.max_room_xy)
  45.         x = random.randint(1, (self.width - w - 1))
  46.         y = random.randint(1, (self.height - h - 1))
  47.  
  48.         return [x, y, w, h]
  49.  
  50.     def room_overlapping(self, room, room_list):
  51.         x = room[0]
  52.         y = room[1]
  53.         w = room[2]
  54.         h = room[3]
  55.  
  56.         for current_room in room_list:
  57.  
  58.             # The rectangles don't overlap if
  59.             # one rectangle's minimum in some dimension
  60.             # is greater than the other's maximum in
  61.             # that dimension.
  62.  
  63.             if (x < (current_room[0] + current_room[2]) and
  64.                 current_room[0] < (x + w) and
  65.                 y < (current_room[1] + current_room[3]) and
  66.                 current_room[1] < (y + h)):
  67.  
  68.                 return True
  69.  
  70.         return False
  71.  
  72.  
  73.     def corridor_between_points(self, x1, y1, x2, y2, join_type='either'):
  74.         if x1 == x2 and y1 == y2 or x1 == x2 or y1 == y2:
  75.             return [(x1, y1), (x2, y2)]
  76.         else:
  77.             # 2 Corridors
  78.             # NOTE: Never randomly choose a join that will go out of bounds
  79.             # when the walls are added.
  80.             join = None
  81.             if join_type is 'either' and set([0, 1]).intersection(
  82.                  set([x1, x2, y1, y2])):
  83.  
  84.                 join = 'bottom'
  85.             elif join_type is 'either' and set([self.width - 1,
  86.                  self.width - 2]).intersection(set([x1, x2])) or set(
  87.                  [self.height - 1, self.height - 2]).intersection(
  88.                  set([y1, y2])):
  89.  
  90.                 join = 'top'
  91.             elif join_type is 'either':
  92.                 join = random.choice(['top', 'bottom'])
  93.             else:
  94.                 join = join_type
  95.  
  96.             if join is 'top':
  97.                 return [(x1, y1), (x1, y2), (x2, y2)]
  98.             elif join is 'bottom':
  99.                 return [(x1, y1), (x2, y1), (x2, y2)]
  100.  
  101.     def join_rooms(self, room_1, room_2, join_type='either'):
  102.         # sort by the value of x
  103.         sorted_room = [room_1, room_2]
  104.         sorted_room.sort(key=lambda x_y: x_y[0])
  105.  
  106.         x1 = sorted_room[0][0]
  107.         y1 = sorted_room[0][1]
  108.         w1 = sorted_room[0][2]
  109.         h1 = sorted_room[0][3]
  110.         x1_2 = x1 + w1 - 1
  111.         y1_2 = y1 + h1 - 1
  112.  
  113.         x2 = sorted_room[1][0]
  114.         y2 = sorted_room[1][1]
  115.         w2 = sorted_room[1][2]
  116.         h2 = sorted_room[1][3]
  117.         x2_2 = x2 + w2 - 1
  118.         y2_2 = y2 + h2 - 1
  119.  
  120.         # overlapping on x
  121.         if x1 < (x2 + w2) and x2 < (x1 + w1):
  122.             jx1 = random.randint(x2, x1_2)
  123.             jx2 = jx1
  124.             tmp_y = [y1, y2, y1_2, y2_2]
  125.             tmp_y.sort()
  126.             jy1 = tmp_y[1] + 1
  127.             jy2 = tmp_y[2] - 1
  128.  
  129.             corridors = self.corridor_between_points(jx1, jy1, jx2, jy2)
  130.             self.corridor_list.append(corridors)
  131.  
  132.         # overlapping on y
  133.         elif y1 < (y2 + h2) and y2 < (y1 + h1):
  134.             if y2 > y1:
  135.                 jy1 = random.randint(y2, y1_2)
  136.                 jy2 = jy1
  137.             else:
  138.                 jy1 = random.randint(y1, y2_2)
  139.                 jy2 = jy1
  140.             tmp_x = [x1, x2, x1_2, x2_2]
  141.             tmp_x.sort()
  142.             jx1 = tmp_x[1] + 1
  143.             jx2 = tmp_x[2] - 1
  144.  
  145.             corridors = self.corridor_between_points(jx1, jy1, jx2, jy2)
  146.             self.corridor_list.append(corridors)
  147.  
  148.         # no overlap
  149.         else:
  150.             join = None
  151.             if join_type is 'either':
  152.                 join = random.choice(['top', 'bottom'])
  153.             else:
  154.                 join = join_type
  155.  
  156.             if join is 'top':
  157.                 if y2 > y1:
  158.                     jx1 = x1_2 + 1
  159.                     jy1 = random.randint(y1, y1_2)
  160.                     jx2 = random.randint(x2, x2_2)
  161.                     jy2 = y2 - 1
  162.                     corridors = self.corridor_between_points(
  163.                         jx1, jy1, jx2, jy2, 'bottom')
  164.                     self.corridor_list.append(corridors)
  165.                 else:
  166.                     jx1 = random.randint(x1, x1_2)
  167.                     jy1 = y1 - 1
  168.                     jx2 = x2 - 1
  169.                     jy2 = random.randint(y2, y2_2)
  170.                     corridors = self.corridor_between_points(
  171.                         jx1, jy1, jx2, jy2, 'top')
  172.                     self.corridor_list.append(corridors)
  173.  
  174.             elif join is 'bottom':
  175.                 if y2 > y1:
  176.                     jx1 = random.randint(x1, x1_2)
  177.                     jy1 = y1_2 + 1
  178.                     jx2 = x2 - 1
  179.                     jy2 = random.randint(y2, y2_2)
  180.                     corridors = self.corridor_between_points(
  181.                         jx1, jy1, jx2, jy2, 'top')
  182.                     self.corridor_list.append(corridors)
  183.                 else:
  184.                     jx1 = x1_2 + 1
  185.                     jy1 = random.randint(y1, y1_2)
  186.                     jx2 = random.randint(x2, x2_2)
  187.                     jy2 = y2_2 + 1
  188.                     corridors = self.corridor_between_points(
  189.                         jx1, jy1, jx2, jy2, 'bottom')
  190.                     self.corridor_list.append(corridors)
  191.  
  192.  
  193.     def gen_level(self):
  194.  
  195.         # build an empty dungeon, blank the room and corridor lists
  196.         for i in range(self.height):
  197.             self.level.append(['stone'] * self.width)
  198.         self.room_list = []
  199.         self.corridor_list = []
  200.  
  201.         max_iters = self.max_rooms * 5
  202.  
  203.         for a in range(max_iters):
  204.             tmp_room = self.gen_room()
  205.  
  206.             if self.rooms_overlap or not self.room_list:
  207.                 self.room_list.append(tmp_room)
  208.             else:
  209.                 tmp_room = self.gen_room()
  210.                 tmp_room_list = self.room_list[:]
  211.  
  212.                 if self.room_overlapping(tmp_room, tmp_room_list) is False:
  213.                     self.room_list.append(tmp_room)
  214.  
  215.             if len(self.room_list) >= self.max_rooms:
  216.                 break
  217.  
  218.         # connect the rooms
  219.         for a in range(len(self.room_list) - 1):
  220.             self.join_rooms(self.room_list[a], self.room_list[a + 1])
  221.  
  222.         # do the random joins
  223.         for a in range(self.random_connections):
  224.             room_1 = self.room_list[random.randint(0, len(self.room_list) - 1)]
  225.             room_2 = self.room_list[random.randint(0, len(self.room_list) - 1)]
  226.             self.join_rooms(room_1, room_2)
  227.  
  228.         # do the spurs
  229.         for a in range(self.random_spurs):
  230.             room_1 = [random.randint(2, self.width - 2), random.randint(
  231.                      2, self.height - 2), 1, 1]
  232.             room_2 = self.room_list[random.randint(0, len(self.room_list) - 1)]
  233.             self.join_rooms(room_1, room_2)
  234.  
  235.         # fill the map
  236.         # paint rooms
  237.         for room_num, room in enumerate(self.room_list):
  238.             for b in range(room[2]):
  239.                 for c in range(room[3]):
  240.                     self.level[room[1] + c][room[0] + b] = 'floor'
  241.  
  242.         # paint corridors
  243.         for corridor in self.corridor_list:
  244.             x1, y1 = corridor[0]
  245.             x2, y2 = corridor[1]
  246.             for width in range(abs(x1 - x2) + 1):
  247.                 for height in range(abs(y1 - y2) + 1):
  248.                     self.level[min(y1, y2) + height][
  249.                         min(x1, x2) + width] = 'floor'
  250.  
  251.             if len(corridor) == 3:
  252.                 x3, y3 = corridor[2]
  253.  
  254.                 for width in range(abs(x2 - x3) + 1):
  255.                     for height in range(abs(y2 - y3) + 1):
  256.                         self.level[min(y2, y3) + height][
  257.                             min(x2, x3) + width] = 'floor'
  258.  
  259.         # paint the walls
  260.         for row in range(1, self.height - 1):
  261.             for col in range(1, self.width - 1):
  262.                 if self.level[row][col] == 'floor':
  263.                     if self.level[row - 1][col - 1] == 'stone':
  264.                         self.level[row - 1][col - 1] = 'wall'
  265.  
  266.                     if self.level[row - 1][col] == 'stone':
  267.                         self.level[row - 1][col] = 'wall'
  268.  
  269.                     if self.level[row - 1][col + 1] == 'stone':
  270.                         self.level[row - 1][col + 1] = 'wall'
  271.  
  272.                     if self.level[row][col - 1] == 'stone':
  273.                         self.level[row][col - 1] = 'wall'
  274.  
  275.                     if self.level[row][col + 1] == 'stone':
  276.                         self.level[row][col + 1] = 'wall'
  277.  
  278.                     if self.level[row + 1][col - 1] == 'stone':
  279.                         self.level[row + 1][col - 1] = 'wall'
  280.  
  281.                     if self.level[row + 1][col] == 'stone':
  282.                         self.level[row + 1][col] = 'wall'
  283.  
  284.                     if self.level[row + 1][col + 1] == 'stone':
  285.                         self.level[row + 1][col + 1] = 'wall'
  286.  
  287.     def gen_tiles_level(self):
  288.  
  289.         for row_num, row in enumerate(self.level):
  290.             tmp_tiles = []
  291.  
  292.             for col_num, col in enumerate(row):
  293.                 if col == 'stone':
  294.                     tmp_tiles.append(self.tiles['stone'])
  295.                 if col == 'floor':
  296.                     tmp_tiles.append(self.tiles['floor'])
  297.                 if col == 'wall':
  298.                     tmp_tiles.append(self.tiles['wall'])
  299.  
  300.             self.tiles_level.append(''.join(tmp_tiles))
  301.  
  302.         print('Room List: ', self.room_list)
  303.         print('\nCorridor List: ', self.corridor_list)
  304.  
  305.         [print(row) for row in self.tiles_level]
  306.  
  307.  
  308. if __name__ == '__main__':
  309.     gen = Generator()
  310.     gen.gen_level()
  311.     gen.gen_tiles_level()


The output should look something like:

python3 generator-1.py 
Room List:  [[45, 26, 7, 9], [29, 18, 8, 10], [21, 28, 9, 5], [7, 26, 7, 7], [21, 16, 9, 7], 
[18, 51, 9, 6], [42, 52, 6, 9], [4, 46, 9, 5], [20, 4, 8, 6], [56, 1, 7, 10], [16, 5, 7, 7], 
[55, 34, 7, 9], [37, 33, 7, 9], [8, 11, 6, 8], [34, 2, 6, 8]]
 
Corridor List:  [[(37, 26), (44, 26)], [(29, 28), (29, 27)], [(14, 28), (20, 28)], 
[(11, 25), (11, 18), (20, 18)], [(24, 23), (24, 50)], [(27, 56), (41, 56)], 
[(13, 46), (46, 46), (46, 51)], [(6, 45), (6, 4), (19, 4)], [(28, 5), (55, 5)], [(23, 5), (55, 5)], 
[(20, 12), (20, 40), (54, 40)], [(44, 38), (54, 38)], [(14, 13), (40, 13), (40, 32)], 
[(14, 17), (37, 17), (37, 10)], [(30, 22), (62, 22), (62, 11)], [(37, 27), (37, 16)], 
[(14, 12), (57, 12), (57, 59)], [(6, 59), (6, 29), (44, 29)]]
                                                       #########
                                 ########              #.......#
                                 #......#              #.......#
     ########################    #......#              #.......#
     #......................######......################.......#
     #.#########...............................................#
     #.#       #............######......################.......#
     #.#       #............#    #......#              #.......#
     #.#       #............#    #......#              #.......#
     #.#       #............#    #......#              #.......#
     #.#########.......######    ####.###              #.......#
     #.#......##.......##############.########################.#
     #.#..................................................#  #.#
     #.#.................................################.#  #.#
     #.#......######.################.##.#              #.#  #.#
     #.#......#    #.##########     #.##.#              #.#  #.#
     #.#......######..........#######.##.#              #.#  #.#
     #.#..............................##.#              #.#  #.#
     #.#..............................##.#              #.#  #.#
     #.####.########..................##.#              #.#  #.#
     #.#  #.#      #..................##.#              #.#  #.#
     #.#  #.#      #..................##.################.####.#
     #.#  #.#      #...........................................#
     #.#  #.#      #.###.####.........##.################.######
     #.#  #.#      #.# #.#  #.........##.#              #.#     
     #.####.###    #.# #.#  #.........##.############   #.#     
     #........#    #.# #.#  #.......................#   #.#     
     #........######.###.####.........##.####.......#   #.#     
     #........................##########.####.......#   #.#     
     #..............................................#   #.#     
     #........######..........##########.####.......#   #.#     
     #........#    #..........#        #.#  #.......#   #.#     
     #........#    #..........#     ####.####.......#   #.#     
     #.########    #.###.######     #.......#.......# ###.##### 
     #.#           #.# #.#          #.......#.......# #.......# 
     #.#           #.# #.#          #.......######### #.......# 
     #.#           #.# #.#          #.......#         #.......# 
     #.#           #.# #.#          #.......###########.......# 
     #.#           #.# #.#          #.........................# 
     #.#           #.###.############.......###########.......# 
     #.#           #..........................................# 
     #.#           #####.############.......###########.......# 
     #.#               #.#          #########         #.......# 
     #.#               #.#                            ###.##### 
     #.#               #.#                              #.#     
   ###.#################.#######################        #.#     
   #...........................................#        #.#     
   #.........###########.#####################.#        #.#     
   #.........#         #.#                   #.#        #.#     
   #.........#         #.#                   #.#        #.#     
   #.........#   #######.###                 #.#        #.#     
   ###.#######   #.........#             #####.##       #.#     
     #.#         #.........#             #......#       #.#     
     #.#         #.........#             #......#       #.#     
     #.#         #.........#             #......#       #.#     
     #.#         #.........###############......#       #.#     
     #.#         #..............................#       #.#     
     #.#         #########################......#       #.#     
     #.#                                 #......#       #.#     
     #.#                                 #......#       #.#     
     ###                                 #......#       ###     
                                         ########


The room list is in [x1, y1, width, height] format, and the corridor list is in [(x1, y1), (x2, y2), ?(x3, y3)] format where (x3, y3) only exist if a hallway has an 'L' bend. I'm still using a distant descendant of this generator for my as of yet unreleased roguelike. I hope others find it useful.

Personal tools