Last active
February 5, 2019 21:08
-
-
Save FractalWire/e9ab0e04c319489a1e6b28f18fc86d9a to your computer and use it in GitHub Desktop.
A map generator for roguelike games
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 = 8 | |
ROOM_HEIGHT = 8 | |
MAX_FAILED_ROOM = 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(2, dim) for dim in [ROOM_WIDTH, ROOM_HEIGHT]] | |
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: | |
xs.pop(1) | |
if y0 <= 0: | |
ys.pop(0) | |
if y1 >= MAP_HEIGHT-1: | |
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 = room_size() | |
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(50, True, 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