Skip to content

Instantly share code, notes, and snippets.

@FractalWire
Created February 7, 2019 15:39
Show Gist options
  • Save FractalWire/5f7150366faf2f7cdbcb1b4c2f1e93b3 to your computer and use it in GitHub Desktop.
Save FractalWire/5f7150366faf2f7cdbcb1b4c2f1e93b3 to your computer and use it in GitHub Desktop.
A map generator for roguelike a bit optimized
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