Skip to content

Instantly share code, notes, and snippets.

@FractalWire
Last active February 5, 2019 22:36
Show Gist options
  • Save FractalWire/845ea98a86f870e4bd49d673fb22db34 to your computer and use it in GitHub Desktop.
Save FractalWire/845ea98a86f870e4bd49d673fb22db34 to your computer and use it in GitHub Desktop.
A map generator for roguelike games with corridors
import curses
import numpy as np
import random
from enum import Enum
from typing import Any, List, Tuple, Callable
import time
MAP_WIDTH = 80
MAP_HEIGHT = 25
ROOM_WIDTH = 12
ROOM_HEIGHT = 8
CORRIDOR_WIDTH = 8
CORRIDOR_HEIGHT = 5
MAX_FAILED_ROOM = 10
CORRIDOR_CHANCES = 90
ROOM_CHANCES = 10
FPS = 10
DELTA_TIME = 1 / FPS
class Tiles(Enum):
EMPTY = ord('.')
WALL = ord('#')
RIGHT = ord('O')
WRONG = ord('X')
CORNER = ord('+')
def generate_map(max_rooms: int, do_print: bool = False, renderer:
Callable[[np.array], None] = None)->np.array:
map_ = np.array([[Tiles.WALL]*MAP_WIDTH]*MAP_HEIGHT)
corners = []
def room_size()->List[int]:
return [random.randrange(1, dim) for dim in [ROOM_WIDTH, ROOM_HEIGHT]]
def corridor_size()->List[int]:
s1 = [0, random.randrange(1, CORRIDOR_HEIGHT)]
s2 = [random.randrange(1, CORRIDOR_WIDTH), 0]
return random.choice([s1, s2])
def update_corners()->None:
nonlocal map_, corners
for corner in corners:
x, y = corner
neighbours = map_[[[y-1, y+1], [y, y]], [[x, x], [x-1, x+1]]]
walls_around = len(neighbours[neighbours == Tiles.WALL])
if walls_around < 2:
map_[y, x] = Tiles.EMPTY
corners.remove(corner)
def add_corners(x0: int, y0: int, x1: int, y1: int) -> None:
nonlocal map_, corners
xs = [x0, x1]
ys = [y0, y1]
if x0 <= 0:
xs.pop(0)
if x1 >= MAP_WIDTH-1 or x0 == x1:
xs.pop(1)
if y0 <= 0:
ys.pop(0)
if y1 >= MAP_HEIGHT-1 or y0 == y1:
ys.pop(1)
temp_corners = [(x, y) for x in xs for y in ys]
for x, y in temp_corners:
neighbours = map_[[[y-1, y+1], [y, y]], [[x, x], [x-1, x+1]]]
walls_around = len(neighbours[neighbours == Tiles.WALL])
if walls_around >= 2:
corners += [(x, y)]
map_[y, x] = Tiles.CORNER
def place_first_room()->None:
nonlocal map_, corners
width, height = room_size()
x0, y0, x1, y1 = 0, 0, MAP_WIDTH, MAP_HEIGHT
while not (x1 < MAP_WIDTH and y1 < MAP_HEIGHT):
x0, y0 = [random.randrange(dim) for dim in [MAP_WIDTH, MAP_HEIGHT]]
x1, y1 = x0+width, y0+height
map_[y0:y1+1, x0:x1+1] = Tiles.EMPTY
add_corners(x0, y0, x1, y1)
def place_room(corner: Tuple[int, int], w: int, h: int)->None:
nonlocal map_, corners
cx, cy = corner
sizes = [(width, height) for width in (-w, w) for height in (-h, h)]
for width, height in sizes:
x, y = cx+width, cy+height
x0, x1 = min(x, cx), max(x, cx)
y0, y1 = min(y, cy), max(y, cy)
x0 = max(0, x0)
y0 = max(0, y0)
x1 = min(x1, MAP_WIDTH-1)
y1 = min(y1, MAP_HEIGHT-1)
room = map_[y0:y1+1, x0:x1+1]
if (do_print):
final_map = np.array(
[[Tiles.WALL]*(MAP_WIDTH+2)]*(MAP_HEIGHT+2))
final_map[1:-1, 1:-1] = map_
p_room = np.copy(room)
p_room[p_room == Tiles.EMPTY] = Tiles.WRONG
p_room[p_room == Tiles.WALL] = Tiles.RIGHT
final_map[y0+1:y1+2, x0+1:x1+2] = p_room
renderer(final_map)
if x0 < 0 or y0 < 0 or not (x1 < MAP_WIDTH and y1 < MAP_HEIGHT):
continue
if Tiles.EMPTY in room:
continue
map_[y0:y1+1, x0:x1+1] = Tiles.EMPTY
add_corners(x0, y0, x1, y1)
return True
else:
return False
place_first_room()
room_cnt = 1
failed_room_placement = 0
while room_cnt < max_rooms and failed_room_placement < MAX_FAILED_ROOM:
shuffled_corners = random.sample(corners, len(corners))
width, height = random.choices([room_size, corridor_size], [
ROOM_CHANCES, CORRIDOR_CHANCES])[0]()
while shuffled_corners != []:
corner = shuffled_corners.pop()
if place_room(corner, width, height):
room_cnt += 1
failed_room_placement = 0
update_corners()
break
else:
failed_room_placement += 1
return map_
last_time = time.perf_counter()
def render(map_: np.array, stdscr: Any)->None:
global last_time
stdscr.clear()
dt = time.perf_counter() - last_time
if dt < DELTA_TIME:
time.sleep(DELTA_TIME-dt)
for y, line in enumerate(map_):
for x, ch in enumerate(line):
stdscr.addch(y, x, ch.value, curses.color_pair(ch.value))
stdscr.refresh()
last_time = time.perf_counter()
def main()->None:
dt = 0
gen_num = 100
try:
stdscr = curses.initscr()
curses.cbreak()
curses.noecho()
curses.curs_set(False)
curses.start_color()
curses.init_color(curses.COLOR_BLACK, 0, 0, 0)
curses.init_color(255, 600, 600, 600)
curses.init_pair(Tiles.EMPTY.value, curses.COLOR_WHITE,
curses.COLOR_BLACK)
curses.init_pair(Tiles.WALL.value, 255, curses.COLOR_BLACK)
curses.init_pair(Tiles.CORNER.value,
curses.COLOR_BLUE, curses.COLOR_BLACK)
curses.init_pair(Tiles.RIGHT.value, curses.COLOR_GREEN,
curses.COLOR_BLACK)
curses.init_pair(Tiles.WRONG.value, curses.COLOR_RED,
curses.COLOR_BLACK)
map_ = generate_map(100, False, lambda x: render(x, stdscr))
t = time.perf_counter()
for i in range(gen_num):
generate_map(False)
dt = time.perf_counter() - t
finally:
curses.endwin()
print("#"*(MAP_WIDTH+2))
for line in map_:
print("#"+''.join(chr(e.value) for e in line).replace("+", ".")+"#")
print("#"*(MAP_WIDTH+2))
print()
print(f"{gen_num} map generated afer {round(dt,3)} seconds "
f"or {round(gen_num/dt,1)} maps/seconds")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment