def test_blob_math_contain(): # These rectangles look like this: # xxxxx # x###x # x###x # x###x # xxxxx rect1 = Rectangle(origin=Point(0, 0), size=Size(5, 5)) rect2 = Rectangle(origin=Point(1, 1), size=Size(3, 3)) blob1 = Blob.from_rectangle(rect1) blob2 = Blob.from_rectangle(rect2) union_blob = blob1 + blob2 assert union_blob.area == blob1.area assert union_blob.height == blob1.height left_blob = blob1 - blob2 assert left_blob.area == 16 assert left_blob.height == 5 assert left_blob.spans == { 0: (Span(0, 4), ), 1: (Span(0, 0), Span(4, 4)), 2: (Span(0, 0), Span(4, 4)), 3: (Span(0, 0), Span(4, 4)), 4: (Span(0, 4), ), } right_blob = blob2 - blob1 assert right_blob.area == 0 assert right_blob.height == 0 assert right_blob.spans == {}
def test_blob_math_overlap(): # These rectangles look like this: # xxx # x##x # x##x # xxx rect1 = Rectangle(origin=Point(0, 0), size=Size(3, 3)) rect2 = Rectangle(origin=Point(1, 1), size=Size(3, 3)) blob1 = Blob.from_rectangle(rect1) blob2 = Blob.from_rectangle(rect2) union_blob = blob1 + blob2 assert union_blob.area == 14 left_blob = blob1 - blob2 assert left_blob.area == 5 assert left_blob.height == 3 assert left_blob.spans == { 0: (Span(0, 2), ), 1: (Span(0, 0), ), 2: (Span(0, 0), ), } right_blob = blob2 - blob1 assert right_blob.area == 5 assert right_blob.height == 3 assert right_blob.spans == { 1: (Span(3, 3), ), 2: (Span(3, 3), ), 3: (Span(1, 3), ), }
def test_blob_math_disjoint(): # These rectangles look like this: # xxx # xxx # xxx xxx # xxx # xxx rect1 = Rectangle(origin=Point(0, 0), size=Size(3, 3)) rect2 = Rectangle(origin=Point(6, 2), size=Size(3, 3)) blob1 = Blob.from_rectangle(rect1) blob2 = Blob.from_rectangle(rect2) union_blob = blob1 + blob2 assert union_blob.area == blob1.area + blob2.area assert union_blob.area == rect1.area + rect2.area assert union_blob.height == 5 left_blob = blob1 - blob2 from pprint import pprint pprint(blob1.spans) pprint(blob2.spans) pprint(left_blob.spans) assert left_blob.area == blob1.area assert left_blob == blob1 right_blob = blob2 - blob1 from pprint import pprint pprint(blob1.spans) pprint(blob2.spans) pprint(right_blob.spans) assert right_blob.area == blob2.area assert right_blob == blob2
def __init__(self, size): self.rect = size.to_rect(Point.origin()) self.entity_positions = WeakKeyDictionary() self.tiles = { point: Tile(self, point) for point in self.rect.iter_points() }
def __init__(self, size): self.rect = size.to_rect(Point.origin()) # TODO i think using types instead of entities /most of the time/ is # more trouble than it's worth self._arch_grid = {point: CaveWall for point in self.rect.iter_points()} self._item_grid = {point: [] for point in self.rect.iter_points()} self._creature_grid = {point: None for point in self.rect.iter_points()} self.floor_spaces = set()
def __init__(self, size): self.rect = size.to_rect(Point.origin()) self.entity_positions = WeakKeyDictionary() self.portal_index = {} self.tiles = { point: Tile(self, point) for point in self.rect.iter_points() }
def __init__(self, size): self.rect = size.to_rect(Point.origin()) # TODO i think using types instead of entities /most of the time/ is # more trouble than it's worth self._arch_grid = { point: CaveWall for point in self.rect.iter_points()} self._item_grid = {point: [] for point in self.rect.iter_points()} self._creature_grid = { point: None for point in self.rect.iter_points()} self.floor_spaces = set()
def randomize(cls, region, *, minimum_size=Size(5, 5)): """Place a room randomly in a region, randomizing its size and position. """ # TODO need to guarantee the region is big enough size = Size( random_normal_range(minimum_size.width, region.width), random_normal_range(minimum_size.height, region.height), ) left = region.left + random.randint(0, region.width - size.width) top = region.top + random.randint(0, region.height - size.height) rect = Rectangle(Point(left, top), size) return cls(rect)
def _generate_river(self, noise): # TODO seriously starting to feel like i need a Feature type for these # things? like, passing `noise` around is a really weird way to go # about this. what would the state even look like though? ''' # TODO i think this needs another flooding algorithm, which probably # means it needs to be a lot simpler and faster... noise_factory = discrete_perlin_noise_factory( *self.region.size, resolution=2, octaves=1) noise = { point: abs(noise_factory(*point) - 0.5) * 2 for point in self.region.iter_points() } for point, n in noise.items(): if n < 0.2: self.map_canvas.set_architecture(point, e.Water) return ''' # Build some Blob internals representing the two halves of the river. left_side = {} right_side = {} river = {} center_factory = discrete_perlin_noise_factory( self.region.height, resolution=3) width_factory = discrete_perlin_noise_factory( self.region.height, resolution=6, octaves=2) center = random_normal_int( self.region.center().x, self.region.width / 4 / 3) for y in self.region.range_height(): center += (center_factory(y) - 0.5) * 3 width = width_factory(y) * 2 + 5 x0 = int(center - width / 2) x1 = int(x0 + width + 0.5) for x in range(x0, x1 + 1): self.map_canvas.set_architecture(Point(x, y), e.Water) left_side[y] = (Span(self.region.left, x0 - 1),) right_side[y] = (Span(x1 + 1, self.region.right),) river[y] = (Span(x0, x1),) return Blob(left_side), Blob(river), Blob(right_side)
def __init__(self, size): self.rect = size.to_rect(Point.origin()) self.arch_grid = {point: CaveWall for point in self.rect.iter_points()} self.item_grid = {point: [] for point in self.rect.iter_points()} self.creature_grid = {point: None for point in self.rect.iter_points()}
def generate(self): self.map_canvas.clear(CaveWall) # First create a bunch of hallways and rooms. # For now, just carve a big area, run a hallway through the middle, and # divide either side into rooms. area = Room.randomize(self.region, minimum_size=self.region.size // 2) area.draw_to_canvas(self.map_canvas) center = area.rect.center() y0 = center.y - 2 y1 = center.y + 2 hallway = Rectangle(origin=Point(area.rect.left, center.y - 2), size=Size(area.rect.width, 5)) Room(hallway).draw_to_canvas(self.map_canvas) top_space = area.rect.replace(bottom=hallway.top) bottom_space = area.rect.replace(top=hallway.bottom) rooms = [] for orig_space in (top_space, bottom_space): space = orig_space # This includes walls! minimum_width = 7 # Note that the rooms overlap where they touch, so we subtract one # from both the total width and the minimum width, in effect # ignoring all the walls on one side maximum_rooms = (space.width - 1) // (minimum_width - 1) # The maximum number of rooms that will fit also affects how much # wiggle room we're willing to have. For example, if at most 3 rooms # will fit, then generating 2 rooms is also reasonable. But if 10 # rooms will fit, generating 2 rooms is a bit silly. We'll arbitrarily # use 1/3 the maximum as the minimum. (Plus 1, to avoid rounding down # to zero.) minimum_rooms = maximum_rooms // 6 + 1 num_rooms = random_normal_range(minimum_rooms, maximum_rooms) # TODO normal distribution doesn't have good results here. think # more about how people use rooms -- often many of similar size, # with some exceptions. also different shapes, bathrooms or # closets nestled together, etc. while num_rooms > 1: # Now we want to divide a given amount of space into n chunks, where # the size of each chunk is normally-distributed. I have no idea how # to do this in any strict mathematical sense, so instead we'll just # carve out one room at a time and hope for the best. min_width = minimum_width avg_width = (space.width - 1) // num_rooms + 1 max_width = space.width - (minimum_width - 1) * (num_rooms - 1) room_width = random_normal_int(avg_width, min(max_width - avg_width, avg_width - min_width) // 3) room = space.replace(right=space.left + room_width - 1) rooms.append(room) space = space.replace(left=room.right) num_rooms -= 1 rooms.append(space) for rect in rooms: Room(rect).draw_to_canvas(self.map_canvas) from flax.component import Lockable # Add some doors for funsies. locked_room = random.choice(rooms) for rect in rooms: x = random.randrange(rect.left + 1, rect.right - 1) if rect.top > hallway.top: side = Direction.down else: side = Direction.up point = rect.edge_point(side.opposite, x, 0) door = e.Door(Lockable(locked=rect is locked_room)) self.map_canvas.set_architecture(point, door) self.hallway_area = Blob.from_rectangle(hallway) self.locked_area = Blob.from_rectangle(locked_room) self.rooms_area = reduce(operator.add, (Blob.from_rectangle(rect) for rect in rooms if rect is not locked_room))
def generate(self): self.map_canvas.clear(Floor) # So what I want here is to have a cave system with a room in the # middle, then decay the room. # Some constraints: # - the room must have a wall where the entrance could go, which faces # empty space # - a wall near the entrance must be destroyed # - the player must start in a part of the cave connected to the # destroyed entrance # - none of the decay applied to the room may block off any of its # interesting features # TODO it would be nice if i could really write all this without ever # having to hardcode a specific direction, so the logic could always be # rotated freely side = random.choice([Direction.left, Direction.right]) # TODO assert region is big enough room_size = Size( random_normal_range(9, int(self.region.width * 0.4)), random_normal_range(9, int(self.region.height * 0.4)), ) room_position = self.region.center() - room_size // 2 room_position += Point( random_normal_int(0, self.region.width * 0.1), random_normal_int(0, self.region.height * 0.1), ) room_rect = Rectangle(room_position, room_size) self.room_region = room_rect room = Room(room_rect) cave_area = ( Blob.from_rectangle(self.region) - Blob.from_rectangle(room_rect) ) self.cave_region = cave_area walls = [point for (point, _) in self.region.iter_border()] floors = [] for point, edge in room_rect.iter_border(): if edge is side or edge.adjacent_to(side): floors.append(point) floors.append(point + side) generate_caves( self.map_canvas, cave_area, CaveWall, force_walls=walls, force_floors=floors, ) room.draw_to_canvas(self.map_canvas) # OK, now draw a gate in the middle of the side wall if side is Direction.left: x = room_rect.left else: x = room_rect.right mid_y = room_rect.top + room_rect.height // 2 if room_rect.height % 2 == 1: min_y = mid_y - 1 max_y = mid_y + 1 else: min_y = mid_y - 2 max_y = mid_y + 1 for y in range(min_y, max_y + 1): self.map_canvas.set_architecture(Point(x, y), KadathGate) # Beat up the border of the room near the gate y = random.choice( tuple(range(room_rect.top, min_y)) + tuple(range(max_y + 1, room_rect.bottom)) ) for dx in range(-2, 3): for dy in range(-2, 3): point = Point(x + dx, y + dy) # TODO i think what i may want is to have the cave be a # "Feature", where i can check whether it has already claimed a # tile, or draw it later, or whatever. if self.map_canvas._arch_grid[point] is not CaveWall: distance = abs(dx) + abs(dy) ruination = random_normal_range(0, 0.2) + distance * 0.2 self.map_canvas.set_architecture( point, e.Rubble(Breakable(ruination))) # And apply some light ruination to the inside of the room border = list(room_rect.iter_border()) # TODO don't do this infinitely; give up after x tries while True: point, edge = random.choice(border) if self.map_canvas._arch_grid[point + edge] is CaveWall: break self.map_canvas.set_architecture(point, CaveWall) self.map_canvas.set_architecture(point - edge, CaveWall) # TODO this would be neater if it were a slightly more random pattern for direction in ( Direction.up, Direction.down, Direction.left, Direction.right): self.map_canvas.set_architecture( point - edge + direction, CaveWall)
def generate(self): # This noise is interpreted roughly as the inverse of "frequently # travelled" -- low values are walked often (and are thus short grass), # high values are left alone (and thus are trees). noise_factory = discrete_perlin_noise_factory( *self.region.size, resolution=6) noise = { point: noise_factory(*point) for point in self.region.iter_points() } local_minima = set() for point, n in noise.items(): # We want to ensure that each "walkable region" is connected. # First step is to collect all local minima -- any walkable tile is # guaranteed to be conneted to one. if all(noise[npt] >= n for npt in point.neighbors if npt in noise): local_minima.add(point) if n < 0.3: arch = CutGrass elif n < 0.6: arch = Grass else: arch = Tree self.map_canvas.set_architecture(point, arch) left_bank, river_blob, right_bank = self._generate_river(noise) # Decide where bridges should go. They can only cross where there's # walkable space on both sides, so find all such areas. # TODO maybe a nicer api for testing walkability here # TODO this doesn't detect a walkable area on one side that has no # walkable area on the other side, and tbh i'm not sure what to do in # such a case anyway. could forcibly punch a path through the trees, i # suppose? that's what i'll have to do anyway, right? # TODO this will break if i ever add a loop in the river, but tbh i # have no idea how to draw bridges in that case new_block = True start = None end = None blocks = [] for y, (span,) in river_blob.spans.items(): if self.map_canvas._arch_grid[Point(span.start - 1, y)] is not Tree and \ self.map_canvas._arch_grid[Point(span.end + 1, y)] is not Tree: if new_block: start = y end = y new_block = False else: end = y else: if not new_block: blocks.append((start, end)) new_block = True if not new_block: blocks.append((start, end)) for start, end in blocks: y = random_normal_range(start, end) span = river_blob.spans[y][0] local_minima.add(Point(span.start - 1, y)) local_minima.add(Point(span.end + 1, y)) for x in span: self.map_canvas.set_architecture(Point(x, y), e.Bridge) # Consider all local minima along the edges, as well. for x in self.region.range_width(): for y in (self.region.top, self.region.bottom): point = Point(x, y) n = noise[point] if (n < noise.get(Point(x - 1, y), 1) and n < noise.get(Point(x + 1, y), 1)): local_minima.add(point) for y in self.region.range_height(): for x in (self.region.left, self.region.right): point = Point(x, y) n = noise[point] if (n < noise.get(Point(x, y - 1), 1) and n < noise.get(Point(x, y + 1), 1)): local_minima.add(point) for point in local_minima: if point not in river_blob: self.map_canvas.set_architecture(point, e.Dirt) for blob in (left_bank, right_bank): paths = self.flood_valleys(blob, local_minima, noise) for path_point in paths: self.map_canvas.set_architecture(path_point, e.Dirt) # Whoops time for another step: generating a surrounding cave wall. for edge in Direction.orthogonal: width = self.region.edge_length(edge) wall_noise = discrete_perlin_noise_factory(width, resolution=6) for n in self.region.edge_span(edge): offset = int(wall_noise(n) * 4 + 1) for m in range(offset): point = self.region.edge_point(edge, n, m) self.map_canvas.set_architecture(point, e.CaveWall)
def test_blob_create(): rect = Rectangle(origin=Point(0, 0), size=Size(5, 5)) blob = Blob.from_rectangle(rect) assert blob.area == rect.area assert blob.height == rect.height
def rows(self): for y in self.rect.range_height(): yield (self.tiles[Point(x, y)] for x in self.rect.range_width())