This commit is contained in:
Yohann D'ANELLO 2021-01-08 16:55:02 +01:00
parent 9b853324ad
commit afaa9d17cd
Signed by: ynerant
GPG Key ID: 3A75C55819C8CF85
3 changed files with 101 additions and 82 deletions

View File

@ -194,11 +194,13 @@ class Map:
self.add_entity(dictclasses[entisave["type"]](**entisave)) self.add_entity(dictclasses[entisave["type"]](**entisave))
@staticmethod @staticmethod
def neighbourhood(grid, y, x, large=False, oob=False): def neighbourhood(grid: List[List["Tile"]], y: int, x: int,
large: bool = False, oob: bool = False) \
-> List[List[int]]:
""" """
Returns up to 8 nearby coordinates, in a 3x3 square around the input coordinate if large is Returns up to 8 nearby coordinates, in a 3x3 square around the input
set to True, or in a 5-square cross by default. Does not return coordinates if they are out coordinate if large is set to True, or in a 5-square cross by default.
of bounds. Does not return coordinates if they are out of bounds.
""" """
height, width = len(grid), len(grid[0]) height, width = len(grid), len(grid[0])
neighbours = [] neighbours = []
@ -208,8 +210,8 @@ class Map:
else: else:
dyxs = [[0, -1], [0, 1], [-1, 0], [1, 0]] dyxs = [[0, -1], [0, 1], [-1, 0], [1, 0]]
for dy, dx in dyxs: for dy, dx in dyxs:
if oob or (0 <= y+dy < height and 0 <= x+dx < width): if oob or (0 <= y + dy < height and 0 <= x + dx < width):
neighbours.append([y+dy, x+dx]) neighbours.append([y + dy, x + dx])
return neighbours return neighbours

View File

@ -1,39 +1,42 @@
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse # Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from enum import auto, Enum from random import random, randint, shuffle
from random import choice, random, randint, shuffle from typing import List, Tuple
from ..interfaces import Map, Tile from ..interfaces import Map, Tile
DEFAULT_PARAMS = { DEFAULT_PARAMS = {
"width" : 120, "width": 120,
"height" : 35, "height": 35,
"tries" : 300, "tries": 300,
"max_rooms" : 20, "max_rooms": 20,
"max_room_tries" : 15, "max_room_tries": 15,
"cross_room" : 1, "cross_room": 1,
"corridor_chance" : .6, "corridor_chance": .6,
"min_v_corr" : 2, "min_v_corr": 2,
"max_v_corr" : 6, "max_v_corr": 6,
"min_h_corr" : 4, "min_h_corr": 4,
"max_h_corr" : 12, "max_h_corr": 12,
"large_circular_room" : .10, "large_circular_room": .10,
"circular_holes" : .5, "circular_holes": .5,
} }
class Generator: class Generator:
def __init__(self, params: dict = DEFAULT_PARAMS): def __init__(self, params: dict = None):
self.params = params self.params = params or DEFAULT_PARAMS
@staticmethod @staticmethod
def room_fits(level, y, x, room, door_y, door_x, dy, dx): def room_fits(level: List[List[Tile]], y: int, x: int,
room: List[List[Tile]], door_y: int, door_x: int,
dy: int, dx: int) -> bool:
lh, lw = len(level), len(level[0]) lh, lw = len(level), len(level[0])
rh, rw = len(room), len(room[0]) rh, rw = len(room), len(room[0])
if not(0 < y+dy < lh and 0 < x+dx < lw): if not(0 < y + dy < lh and 0 < x + dx < lw):
return False return False
if level[y][x] != Tile.EMPTY or level[y+dy][x+dx] != Tile.FLOOR: if level[y][x] != Tile.EMPTY or level[y + dy][x + dx] != Tile.FLOOR:
return False return False
for ry in range(rh): for ry in range(rh):
for rx in range(rw): for rx in range(rw):
@ -45,24 +48,26 @@ class Generator:
return False return False
# so do all neighbouring tiles bc we may # so do all neighbouring tiles bc we may
# need to place walls there eventually # need to place walls there eventually
for ny, nx in Map.neighbourhood(level, ly, lx, large=True, oob=True): for ny, nx in Map.neighbourhood(level, ly, lx,
large=True, oob=True):
if not(0 <= ny < lh and 0 <= nx < lw) or \ if not(0 <= ny < lh and 0 <= nx < lw) or \
level[ny][nx] != Tile.EMPTY: level[ny][nx] != Tile.EMPTY:
return False return False
return True return True
@staticmethod @staticmethod
def place_room(level, y, x, room, door_y, door_x): def place_room(level: List[List[Tile]], y: int, x: int,
room: List[List[Tile]], door_y: int, door_x: int) -> None:
rh, rw = len(room), len(room[0]) rh, rw = len(room), len(room[0])
# maybe place Tile.DOOR here ? # maybe place Tile.DOOR here ?
level[y][x] = Tile.FLOOR level[y][x] = Tile.FLOOR
for ry in range(rh): for ry in range(rh):
for rx in range(rw): for rx in range(rw):
if room[ry][rx] == Tile.FLOOR: if room[ry][rx] == Tile.FLOOR:
level[y-door_y+ry][x-door_x+rx] = Tile.FLOOR level[y - door_y + ry][x - door_x + rx] = Tile.FLOOR
@staticmethod @staticmethod
def place_walls(level): def place_walls(level: List[List[Tile]]) -> None:
h, w = len(level), len(level[0]) h, w = len(level), len(level[0])
for y in range(h): for y in range(h):
for x in range(w): for x in range(w):
@ -70,22 +75,24 @@ class Generator:
for ny, nx in Map.neighbourhood(level, y, x): for ny, nx in Map.neighbourhood(level, y, x):
if level[ny][nx] == Tile.EMPTY: if level[ny][nx] == Tile.EMPTY:
level[ny][nx] = Tile.WALL level[ny][nx] = Tile.WALL
def corr_meta_info(self): def corr_meta_info(self) -> Tuple[int, int, int, int]:
if random() < self.params["corridor_chance"]: if random() < self.params["corridor_chance"]:
h_sup = randint(self.params["min_v_corr"], \ h_sup = randint(self.params["min_v_corr"],
self.params["max_v_corr"]) if random() < .5 else 0 self.params["max_v_corr"]) if random() < .5 else 0
w_sup = 0 if h_sup else randint(self.params["min_h_corr"], \ w_sup = 0 if h_sup else randint(self.params["min_h_corr"],
self.params["max_h_corr"]) self.params["max_h_corr"])
h_off = h_sup if random() < .5 else 0 h_off = h_sup if random() < .5 else 0
w_off = w_sup if random() < .5 else 0 w_off = w_sup if random() < .5 else 0
return h_sup, w_sup, h_off, w_off return h_sup, w_sup, h_off, w_off
return 0, 0, 0, 0 return 0, 0, 0, 0
def attach_door(self, room, h_sup, w_sup, h_off, w_off): def attach_door(self, room: List[List[Tile]], h_sup: int, w_sup: int,
l = h_sup + w_sup h_off: int, w_off: int) \
-> Tuple[int, int, int, int]:
length = h_sup + w_sup
dy, dx = 0, 0 dy, dx = 0, 0
if l > 0: if length > 0:
if h_sup: if h_sup:
dy = -1 if h_off else 1 dy = -1 if h_off else 1
else: else:
@ -103,77 +110,85 @@ class Generator:
y, x = pos // rw, pos % rw y, x = pos // rw, pos % rw
if room[y][x] == Tile.EMPTY: if room[y][x] == Tile.EMPTY:
# verify we are pointing away from a floor tile # verify we are pointing away from a floor tile
if not(0 <= y-dy < rh and 0 <= x-dx < rw) or room[y-dy][x-dx] != Tile.FLOOR: if not(0 <= y - dy < rh and 0 <= x - dx < rw) \
or room[y - dy][x - dx] != Tile.FLOOR:
continue continue
# verify there's no other floor tile around us # verify there's no other floor tile around us
for ny, nx in [[y+dy, x+dx], [y-dx, x-dy], [y+dx, x+dy]]: for ny, nx in [[y + dy, x + dx], [y - dx, x - dy],
if 0 <= ny < rh and 0 <= nx < rw and room[ny][nx] != Tile.EMPTY: [y + dx, x + dy]]:
if 0 <= ny < rh and 0 <= nx < rw \
and room[ny][nx] != Tile.EMPTY:
break break
else: else:
for i in range(l): for i in range(length):
if room[y+i*dy][x+i*dx] != Tile.EMPTY: if room[y + i * dy][x + i * dx] != Tile.EMPTY:
break break
else: else:
for i in range(l): for i in range(length):
room[y+i*dy][x+i*dx] = Tile.FLOOR room[y + i * dy][x + i * dx] = Tile.FLOOR
break break
return y+l*dy, x+l*dx, dy, dx return y + length * dy, x + length * dx, dy, dx
def create_circular_room(self) -> Tuple[List[List[Tile]], int, int,
def create_circular_room(self): int, int]:
if random() < self.params["large_circular_room"]: if random() < self.params["large_circular_room"]:
r = randint(5, 10) r = randint(5, 10)
else: else:
r = randint(2, 4) r = randint(2, 4)
room = [] room = []
h_sup, w_sup, h_off, w_off = self.corr_meta_info() h_sup, w_sup, h_off, w_off = self.corr_meta_info()
height = 2*r+2 height = 2 * r + 2
width = 2*r+2 width = 2 * r + 2
make_hole = r > 6 and random() < self.params["circular_holes"] make_hole = r > 6 and random() < self.params["circular_holes"]
r2 = 0
if make_hole: if make_hole:
r2 = randint(3, r-3) r2 = randint(3, r - 3)
for i in range(height+h_sup): for i in range(height + h_sup):
room.append([]) room.append([])
d = (i-h_off-height//2)**2 d = (i - h_off - height // 2) ** 2
for j in range(width+w_sup): for j in range(width + w_sup):
if d + (j-w_off-width//2)**2 < r**2 and \ if d + (j - w_off - width // 2) ** 2 < r ** 2 and \
(not(make_hole) or d + (j-w_off-width//2)**2 >= r2**2): (not make_hole
or d + (j - w_off - width // 2) ** 2 >= r2 ** 2):
room[-1].append(Tile.FLOOR) room[-1].append(Tile.FLOOR)
else: else:
room[-1].append(Tile.EMPTY) room[-1].append(Tile.EMPTY)
door_y, door_x, dy, dx = self.attach_door(room, h_sup, w_sup, h_off, w_off) door_y, door_x, dy, dx = self.attach_door(room, h_sup, w_sup,
h_off, w_off)
return room, door_y, door_x, dy, dx return room, door_y, door_x, dy, dx
def create_random_room(self): def create_random_room(self) -> Tuple[List[list], int, int, int, int]:
return self.create_circular_room() return self.create_circular_room()
def run(self): def run(self) -> Map:
height, width = self.params["height"], self.params["width"] height, width = self.params["height"], self.params["width"]
level = [[Tile.EMPTY for i in range(width)] for j in range(height)] level = [width * [Tile.EMPTY] for _ignored in range(height)]
# the starting room must have no corridor # the starting room must have no corridor
mem, self.params["corridor_chance"] = self.params["corridor_chance"], 0 mem, self.params["corridor_chance"] = self.params["corridor_chance"], 0
starting_room, _, _, _, _ = self.create_random_room() starting_room, _, _, _, _ = self.create_random_room()
dim_v, dim_h = len(starting_room), len(starting_room[0]) dim_v, dim_h = len(starting_room), len(starting_room[0])
pos_y, pos_x = randint(0, height-dim_v-1), randint(0, width-dim_h-1) pos_y, pos_x = randint(0, height - dim_v - 1),\
randint(0, width - dim_h - 1)
self.place_room(level, pos_y, pos_x, starting_room, 0, 0) self.place_room(level, pos_y, pos_x, starting_room, 0, 0)
if starting_room[0][0] != Tile.FLOOR: if starting_room[0][0] != Tile.FLOOR:
level[pos_y][pos_x] = Tile.EMPTY level[pos_y][pos_x] = Tile.EMPTY
self.params["corridor_chance"] = mem self.params["corridor_chance"] = mem
# find a starting position # find a starting position
sy, sx = randint(0, height-1), randint(0, width-1) sy, sx = randint(0, height - 1), randint(0, width - 1)
while level[sy][sx] != Tile.FLOOR: while level[sy][sx] != Tile.FLOOR:
sy, sx = randint(0, height-1), randint(0, width-1) sy, sx = randint(0, height - 1), randint(0, width - 1)
# now we loop until we've tried enough, or we've added enough rooms # now we loop until we've tried enough, or we've added enough rooms
tries, rooms_built = 0, 0 tries, rooms_built = 0, 0
while tries < self.params["tries"] and rooms_built < self.params["max_rooms"]: while tries < self.params["tries"] \
and rooms_built < self.params["max_rooms"]:
room, door_y, door_x, dy, dx = self.create_random_room() room, door_y, door_x, dy, dx = self.create_random_room()
positions = [i for i in range(height * width)] positions = [i for i in range(height * width)]
@ -185,7 +200,7 @@ class Generator:
rooms_built += 1 rooms_built += 1
break break
tries += 1 tries += 1
# post-processing # post-processing
self.place_walls(level) self.place_walls(level)

View File

@ -3,27 +3,29 @@
import unittest import unittest
from random import randint from random import randint
from typing import List
from squirrelbattle.interfaces import Map, Tile from squirrelbattle.interfaces import Map, Tile
from squirrelbattle.mapgeneration import broguelike from squirrelbattle.mapgeneration import broguelike
def is_connex(grid):
h, w = len(grid), len(grid[0])
y, x = randint(0, h-1), randint(0, w-1)
while not(grid[y][x].can_walk()):
y, x = randint(0, h-1), randint(0, w-1)
queue = Map.neighbourhood(grid, y, x)
while queue != []:
y, x = queue.pop()
if grid[y][x].can_walk():
grid[y][x] = Tile.WALL
queue += Map.neighbourhood(grid, y, x)
return not(any([any([t.can_walk() for t in l]) for l in grid]))
class TestBroguelike(unittest.TestCase): class TestBroguelike(unittest.TestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.generator = broguelike.Generator() self.generator = broguelike.Generator()
def is_connex(self, grid: List[List[Tile]]) -> bool:
h, w = len(grid), len(grid[0])
y, x = randint(0, h - 1), randint(0, w - 1)
while not (grid[y][x].can_walk()):
y, x = randint(0, h - 1), randint(0, w - 1)
queue = Map.neighbourhood(grid, y, x)
while queue:
y, x = queue.pop()
if grid[y][x].can_walk():
grid[y][x] = Tile.WALL
queue += Map.neighbourhood(grid, y, x)
return not any([t.can_walk() for row in grid for t in row])
def test_connexity(self) -> None: def test_connexity(self) -> None:
m = self.generator.run() m = self.generator.run()
self.assertTrue(is_connex(m.tiles)) self.assertTrue(self.is_connex(m.tiles))