Created
February 7, 2019 15:39
-
-
Save FractalWire/5f7150366faf2f7cdbcb1b4c2f1e93b3 to your computer and use it in GitHub Desktop.
A map generator for roguelike a bit optimized
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 IntEnum | |
from typing import Any, List, Tuple, Callable | |
import time | |
import profile | |
MAP_WIDTH = 100 | |
MAP_HEIGHT = 100 | |
ROOM_WIDTH = 8 | |
ROOM_HEIGHT = 8 | |
CORRIDOR_WIDTH = 8 | |
CORRIDOR_HEIGHT = 8 | |
MAX_FAILED_ROOM = 9 | |
MAX_CORNER_MISS = 2 | |
CORRIDOR_CHANCES = 0 | |
ROOM_CHANCES = 50 | |
FPS = 10 | |
DELTA_TIME = 1 / FPS | |
failed_room_counter = 0 | |
gen_aborted = 0 | |
corner_hit = 0 | |
corner_miss = 0 | |
class Tiles(IntEnum): | |
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.ndarray: | |
global failed_room_counter, gen_aborted, corner_hit, corner_miss | |
map_ = np.full((MAP_HEIGHT, MAP_WIDTH), Tiles.WALL, dtype=np.int8) | |
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 isvalid_corner(coord: Tuple[int, int])->bool: | |
nonlocal map_ | |
x, y = coord | |
neighbours = map_[[y-1, y, y+1, y], [x, x-1, x, x+1]] | |
walls_around = (neighbours == Tiles.WALL).sum() | |
isempty_opposite = any(neighbours[i] == neighbours[i+2] == Tiles.EMPTY | |
for i in (0, 1)) | |
return walls_around >= 2 and not isempty_opposite | |
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: | |
if isvalid_corner((x, y)): | |
corners += [[(x, y), 0]] | |
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_index: int, w: int, h: | |
int)->bool: | |
nonlocal map_, corners | |
cx, cy = corners[i][0] | |
if corner[1] >= MAX_CORNER_MISS or not isvalid_corner((cx, cy)): | |
map_[cy, cx] = Tiles.EMPTY | |
del corners[i] | |
return False | |
# sizes = ((width, height) for width in (-w, w) for height in (-h, h)) | |
sizes = (((-1)**(i+1)*x, (-1)**(i+j+1)*y)[::((-1)**j)] | |
for i, x in enumerate((w, w)) for j, y in enumerate((h, h))) | |
for width, height in sizes: | |
x0, x1 = sorted((0, cx, cx+width, MAP_WIDTH-1))[1:3] | |
y0, y1 = sorted((0, cy, cy+height, MAP_HEIGHT-1))[1:3] | |
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 Tiles.EMPTY in room: | |
continue | |
room[:] = 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: | |
width, height = random.choices([room_size, corridor_size], [ | |
ROOM_CHANCES+room_cnt//2, | |
CORRIDOR_CHANCES-room_cnt//2])[0]() | |
for i in range(len(corners))[::-1]: | |
# i = random.randrange(len(corners)) | |
corner = corners[i] | |
success = place_room(corner, width, height) | |
if success: | |
room_cnt += 1 | |
corner_hit += success | |
failed_room_placement -= (failed_room_placement > 0) | |
break | |
else: | |
corner[1] += 1 | |
corner_miss += not success | |
else: | |
failed_room_placement += 1 | |
failed_room_counter += 1 | |
if failed_room_placement >= MAX_FAILED_ROOM: | |
gen_aborted += 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): | |
ch = int(ch) | |
stdscr.addch(y, x, ch, curses.color_pair(ch)) | |
stdscr.refresh() | |
last_time = time.perf_counter() | |
def main()->None: | |
global failed_room_counter, gen_aborted, corner_hit, corner_miss | |
dt = 0 | |
gen_num = 10 | |
room_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, curses.COLOR_WHITE, | |
curses.COLOR_BLACK) | |
curses.init_pair(Tiles.WALL, 255, curses.COLOR_BLACK) | |
curses.init_pair(Tiles.CORNER, | |
curses.COLOR_BLUE, curses.COLOR_BLACK) | |
curses.init_pair(Tiles.RIGHT, curses.COLOR_GREEN, | |
curses.COLOR_BLACK) | |
curses.init_pair(Tiles.WRONG, curses.COLOR_RED, | |
curses.COLOR_BLACK) | |
map_ = generate_map(room_num, False, lambda x: render(x, stdscr)) | |
curses.endwin() | |
t = time.perf_counter() | |
for i in range(gen_num): | |
generate_map(room_num) | |
dt = time.perf_counter() - t | |
finally: | |
curses.endwin() | |
print("#"*(MAP_WIDTH+2)) | |
for line in map_: | |
print("#"+''.join(chr(e) 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") | |
print( f"failed rooms : {failed_room_counter}, gen aborted : {gen_aborted}") | |
print(f"corner hit : {corner_hit}, corner miss : {corner_miss}") | |
print(f" hit-to-miss : {round(corner_hit/corner_miss,5)}") | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment