Last active
February 5, 2019 22:36
-
-
Save FractalWire/845ea98a86f870e4bd49d673fb22db34 to your computer and use it in GitHub Desktop.
A map generator for roguelike games with corridors
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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