import time import random class Room: def __init__(self): self.entrances = [] def __eq__(self, other): return self.bbox == other.bbox @property def bbox(self): return (self.ulx, self.uly, self.lrx, self.lry) def create_entrance(self, direction): if direction == 'north': x = random.randint(self.ulx, self.lrx) y = self.uly - 1 elif direction == 'south': x = random.randint(self.ulx, self.lrx) y = self.lry + 1 elif direction == 'west': x = self.ulx - 1 y = random.randint(self.uly, self.lry) else: x = self.lrx + 1 y = random.randint(self.uly, self.lry) e = {'direction': direction, 'x': x, 'y': y, 'nearest': None, 'painted': False, 'owner':self} self.entrances.append(e) def contains(self, x, y, padding=0): return (self.ulx-padding <= x <= self.lrx+padding) and (self.uly-padding <= y <= self.lry+padding) def does_collide(self, bbox, padding=0): ''' Check whether this Room's bounding box collides with another. If padding is provided, rooms will be considered collided if they are this close to each other, even if not touching. bbbox may be another Room object, or a tuple of the form (ulx, uly, lrx, lry) ''' if isinstance(bbox, Room): bbox = ulx.bbox ulx = bbox[0] - padding uly = bbox[1] - padding lrx = bbox[2] + padding lry = bbox[3] + padding return not (ulx > self.lrx or lrx < self.ulx or uly > self.lry or lry < self.uly) def choose_room_size(minw, maxw, minh, maxh, msquareness): if msquareness == 0: w = random.randint(minw, maxw) h = random.randint(minh, maxh) osquareness = 1 + (1-msquareness) if random.getrandbits(1): # In order to keep the rooms from being stupdly narrow, # we decide one dimension freely and then the other one based # on the minimum squareness and the first dimension # This chooses whether W or H should be the free dimension. w = random.randint(minw, maxw) h = random.randint(max(minh, int(w * msquareness)), min(maxh, int(w * osquareness))) else: h = random.randint(minh, maxh) w = random.randint(max(minw, int(h * msquareness)), min(maxw, int(h * osquareness))) #print('dim', w,h) return (w, h) def push_into_world_bounds(ulx, uly, lrx, lry, worldw, worldh): if (lry - uly) > (worldh-2) or (lrx - ulx) > (worldw-2): raise ValueError('Cannot fit on world!') if ulx < 1: #print('Push right') diff = 1 - ulx ulx += diff lrx += diff if uly < 1: #print('Push down') diff = 1 - uly uly += diff lry += diff if lrx > (worldw-2): #print('Push left') diff = lrx - (worldw-2) ulx -= diff lrx -= diff if lry > (worldh-2): #print('Stick em\' up') diff = lry - (worldh-2) uly -= diff lry -= diff return (ulx, uly, lrx, lry) def distance(x1, x2, y1, y2): d = (x1 - x2) ** 2 d += (y1 - y2) ** 2 return d ** 0.5 def paint(world, character, x, y): world[y][x] = character def generate_dungeon( world_width=128, world_height=64, min_room_width=12, min_room_height=12, max_room_width=30, max_room_height=30, min_room_squareness=0.5, min_room_count=12, max_room_count=25, character_wall='#', character_floor=' ', character_spawnpoint=None, character_exitpoint=None, include_metadata=False, room_replace_attempts=6, force_random_seed=None, ): ''' Returns a list of lists of characters. Each primary list represents a row of the map, and each entry character in that list represents the tile at that column. : PARAMETERS : world_width = the width of the map in characters. world_height = the height of the map in characters. min_room_width = the mininumum horizontal width of any room in characters. max_room_width = the maximum horizontal width of any room in characters. min_room_height = the mininumum vertical height of any room in characters. max_room_height = the maximum vertical height of any room in characters. min_room_squareness = a number between 0 and 1. this helps prevent any rooms from becoming stupidly narrow. a value of 1 means that every room must be perfectly square. A value of 0 means the walls behave independently. 0.5 means that a room can be at most 2x1. etc. min_room_count = the minimum number of rooms. This may not necessarily be met if the world is so tightly packed that the function repeatedly fails to place rooms. *see room_replace_attemps*. max_room_count = the maximum number of rooms. character_wall = the character to represent unwalkable space character_floor = the character to represent walkable space character_spawnpoint = if not None, the character to represent a suggested spawnpoint into the level character_exitpoint = if not None, the character to represent a suggested exitpoint from the level. It might end up in the same room as the spawnpoint include_metadata = append an additional string to the end of the world list containing some additional information about the world you have generated. room_replace_attempts = if the world is tightly packed and the function is having trouble placing a room, how many times should it reroll. Higher numbers means more loops force_random_seed = if not None, set the random seed manually. Good for testing and demonstration. ''' # originally, I had something more like: # world = [[wall] * width] * height # but apparently this does not create unique list objects # and tile assignment was happening to all lines at once. if force_random_seed: random.seed(force_random_seed) world = [[character_wall for x in range(world_width)] for y in range(world_height)] rooms = [] room_count = random.randint(min_room_count, max_room_count) for roomnumber in range(room_count): room = Room() for attempt in range(room_replace_attempts): ulx = random.randint(1, world_width-1) uly = random.randint(1, world_height-1) dimensions = choose_room_size(min_room_width, max_room_width, min_room_height, max_room_height, min_room_squareness) lrx = ulx + dimensions[0] lry = uly + dimensions[1] ulx, uly, lrx, lry = push_into_world_bounds(ulx, uly, lrx, lry, world_width, world_height) collided = False for otherroom in rooms: if otherroom.does_collide((ulx, uly, lrx, lry), padding=4): collided = True break if not collided: # Now we can finalize coordinates room.ulx = ulx room.uly = uly room.lrx = lrx room.lry = lry rooms.append(room) break for room in rooms: # Paint the floors for x in range(room.ulx, room.lrx+1): for y in range(room.uly, room.lry+1): world[y][x] = character_floor if len(room.entrances) > 0: break north, south, east, west = (True, True, True, True) for otherroom in rooms: if otherroom.bbox == room.bbox: continue if north and room.ulx > 2 and otherroom.lry < room.uly: room.create_entrance('north') north = False elif south and room.lry < (world_height -2) and otherroom.uly > room.lry: room.create_entrance('south') south = False elif east and room.lry < (world_width - 2) and otherroom.lrx > (room.lrx+5): room.create_entrance('east') east = False elif west and room.ulx > 2 and otherroom.ulx < (room.ulx-5): room.create_entrance('west') west = False entrances = [room.entrances for room in rooms] entrances = [entrance for sublist in entrances for entrance in sublist] # Match nearest entrances for entrance in entrances: nearest = None x = entrance['x'] y = entrance['y'] for otherentrance in entrances: if entrance['direction'] == otherentrance['direction']: continue # Compare the x and y coordinates, not the dicts directly # because the dicts are interlinked and cause recur depth if x == otherentrance['x'] and y == otherentrance['y']: continue if entrance['owner'] == otherentrance['owner']: continue # Let's try to prevent any stupid connections. if entrance['direction'] == 'north' and otherentrance['y'] > entrance['y']: continue if entrance['direction'] == 'south' and otherentrance['y'] < entrance['y']: continue if entrance['direction'] == 'west' and otherentrance['x'] > entrance['x']: continue if entrance['direction'] == 'east' and otherentrance['x'] < entrance['x']: continue if otherentrance['direction'] == 'north' and otherentrance['y'] < entrance['y']: continue if otherentrance['direction'] == 'south' and otherentrance['y'] > entrance['y']: continue if otherentrance['direction'] == 'west' and otherentrance['x'] < entrance['x']: continue if otherentrance['direction'] == 'east' and otherentrance['x'] > entrance['x']: continue ox = otherentrance['x'] oy = otherentrance['y'] distanceto = distance(x, ox, y, oy) if nearest is None or distanceto < nearest: nearest = distanceto # Can assign both at once because they are each other's closest. entrance['nearest'] = otherentrance # Paint the tunnels for entrance in entrances: if entrance['painted'] is True: continue nearest = entrance['nearest'] if nearest is None: # This happens when there wasn't a suitable nearby entrance continue direction = entrance['direction'] odirection = nearest['direction'] if {direction, odirection} == {'north', 'south'}: major = 'y' minor = 'x' elif {direction, odirection} == {'east', 'west'}: major = 'x' minor = 'y' else: # 90 degree bends require their own handling. boostsx = {'west':-1, 'east':1} boostsy = {'north':-1, 'south':1} x = entrance['x'] + boostsx.get(direction, 0) y = entrance['y'] + boostsy.get(direction, 0) ox = nearest['x'] + boostsx.get(odirection, 0) oy = nearest['y'] + boostsy.get(odirection, 0) paint(world, character_floor, entrance['x'], entrance['y']) paint(world, character_floor, nearest['x'], nearest['y']) #paint(world, character_floor, x, y) #paint(world, character_floor, ox, oy) corner = (ox, y) if entrance['owner'].contains(*corner, padding=1) or nearest['owner'].contains(*corner, padding=1): corner = (x, oy) for xx in range(min(ox, x), max(ox, x)+1): paint(world, character_floor, xx, corner[1]) pass for yy in range(min(oy, y), max(oy, y)+1): paint(world, character_floor, corner[0], yy) pass entrance['painted'] = True nearest['painted'] = True continue paint(world, character_floor, entrance['x'], entrance['y']) # This controls the step of the range() that controls the # upcoming for-loops. # Count up for things at higher coordinates, etc. # Restricts the difference to -1 or 1 major_direction = max(min(nearest[major] - entrance[major], 1), -1) minor_direction = max(min(nearest[minor] - entrance[minor], 1), -1) major_length = abs(entrance[major] - nearest[major]) // 2 minor_length = abs(entrance[minor] - nearest[minor]) boost = (major_length * major_direction) + major_direction # From the current entrance halfway to the other for m in range(major_length): m += 1 m = entrance[major] + (major_direction * m) paint(world, character_floor, **{major: m, minor: entrance[minor]}) # From the halfway point to the other entrance for m in range(major_length): m = nearest[major] - (major_direction * m) paint(world, character_floor, **{major: m, minor: nearest[minor]}) pass # Connect these two half-lengths with the minor axis if minor_direction == 0: paint(world, character_floor, **{minor: entrance[minor], major: entrance[major]+boost}) else: for m in range(entrance[minor], nearest[minor]+minor_direction, minor_direction): paint(world, character_floor, **{minor: m, major: entrance[major]+boost}) pass entrance['painted'] = True nearest['painted'] = True # Suggest a spawn point and exit point for suggestion in [character_spawnpoint, character_exitpoint]: if suggestion is None: continue room = random.choice(rooms) x = random.randint(room.ulx, room.lrx) y = random.randint(room.uly, room.lry) paint(world, suggestion, x, y) if include_metadata: meta = 'rooms: %d, entrances: %d' % (len(rooms), len(entrances)) world.append(meta) return world def PNG_example(filename, **kwargs): from PIL import Image world = generate_dungeon(**kwargs) if kwargs.get('include_metadata', False): world = world[:-1] height = len(world) width = len(world[0]) i = Image.new('RGBA', (width, height)) for (yindex, yline) in enumerate(world): for (xindex, character) in enumerate(yline): if character == '#': value = (0, 0, 0) elif character == ' ': value= (255, 255, 255) elif character == 'I': value= (255, 0, 0) elif character == 'O': value= (0, 255, 0) i.putpixel((xindex, yindex), value) #i = i.resize((width * 2, height * 2)) i.save(filename) def TXT_example(filename, **kwargs): world = generate_dungeon(**kwargs) world = [''.join(yline) for yline in world] world = '\n'.join(world) if not filename.endswith('.txt'): filename += '.txt' out = open(filename, 'w') out.write(world) out.close() PNG_example('example001.png', world_width=512, world_height=64, min_room_width=12, min_room_height=12, max_room_width=30, max_room_height=30, min_room_squareness=0.5, min_room_count=12, max_room_count=25, character_wall='#', character_floor=' ', include_metadata=False, character_spawnpoint='I', character_exitpoint='O', room_replace_attempts=6, force_random_seed=8, ) TXT_example('example002.txt', world_width=128, world_height=64, min_room_width=12, min_room_height=12, max_room_width=30, max_room_height=30, min_room_squareness=0.5, min_room_count=12, max_room_count=25, character_wall='#', character_floor=' ', include_metadata=True, character_spawnpoint='I', character_exitpoint='O', room_replace_attempts=6, force_random_seed=8, ) PNG_example('example003.png', world_width=512, world_height=512, min_room_width=12, min_room_height=12, max_room_width=30, max_room_height=30, min_room_squareness=1, min_room_count=12, max_room_count=25, character_wall='#', character_floor=' ', include_metadata=True, character_spawnpoint='I', character_exitpoint='O', room_replace_attempts=6, force_random_seed=88, ) PNG_example('example004.png', world_width=512, world_height=512, min_room_width=6, min_room_height=6, max_room_width=80, max_room_height=80, min_room_squareness=0, min_room_count=12, max_room_count=25, character_wall='#', character_floor=' ', include_metadata=True, character_spawnpoint='I', character_exitpoint='O', room_replace_attempts=6, force_random_seed=7777, ) TXT_example('example005.txt', world_width=256, world_height=256, min_room_width=6, min_room_height=6, max_room_width=30, max_room_height=30, min_room_squareness=1, min_room_count=3, max_room_count=8, character_wall='#', character_floor=' ', include_metadata=True, character_spawnpoint='I', character_exitpoint='O', room_replace_attempts=6, force_random_seed=1212, )