class Room: """A room, which has not yet been drawn. Performs some light randomization of the room shape. """ MINIMUM_SIZE = Size(5, 5) def __init__(self, region): self.region = region self.size = Size( random_normal_range(self.MINIMUM_SIZE.width, region.width), random_normal_range(self.MINIMUM_SIZE.height, region.height), ) left = region.left + random.randint(0, region.width - self.size.width) top = region.top + random.randint(0, region.height - self.size.height) self.rect = Rectangle(Point(left, top), self.size) def draw_to_canvas(self, canvas): assert self.rect in canvas.rect for point in self.rect.iter_points(): canvas.set_architecture(point, e.Floor) # Top and bottom for x in self.rect.range_width(): canvas.set_architecture(Point(x, self.rect.top), Wall) canvas.set_architecture(Point(x, self.rect.bottom), Wall) # Left and right (will hit corners again, whatever) for y in self.rect.range_height(): canvas.set_architecture(Point(self.rect.left, y), Wall) canvas.set_architecture(Point(self.rect.right, y), Wall)
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 render(self, size, focus=False): size = Size(*size) map = self.world.current_map map_rect = map.rect player_position = map.find(self.world.player).position if not self.viewport: # Let's pretend the map itself is the viewport, and the below logic # can adjust it as necessary. self.viewport = self.world.current_map.rect horizontal = self._adjust_viewport( self.viewport.horizontal_span, size.width, player_position.x, map.rect.horizontal_span ) vertical = self._adjust_viewport( self.viewport.vertical_span, size.height, player_position.y, map.rect.vertical_span ) self.viewport = Rectangle.from_spans(horizontal=horizontal, vertical=vertical) # viewport is from the pov of the map; negate it to get how much space # is added or removed around the map pad_left = -self.viewport.left pad_top = -self.viewport.top pad_right = (size.width - pad_left) - map_rect.width pad_bottom = (size.height - pad_top) - map_rect.height # TODO it's unclear when you're near the edge of the map, which i hate. # should either show a clear border past the map edge, or show some # kinda fade or whatever along a cut-off edge map_canvas = urwid.CompositeCanvas(CellCanvas(map)) map_canvas.pad_trim_left_right(pad_left, pad_right) map_canvas.pad_trim_top_bottom(pad_top, pad_bottom) return map_canvas
def __init__(self, region): self.region = region self.size = Size( random_normal_range(self.MINIMUM_SIZE.width, region.width), random_normal_range(self.MINIMUM_SIZE.height, region.height), ) left = region.left + random.randint(0, region.width - self.size.width) top = region.top + random.randint(0, region.height - self.size.height) self.rect = Rectangle(Point(left, top), self.size)
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 render(self, size, focus=False): size = Size(*size) map = self.world.current_map map_rect = map.rect player_position = map.find(self.world.player).position if not self.viewport: # Let's pretend the map itself is the viewport, and the below logic # can adjust it as necessary. self.viewport = self.world.current_map.rect horizontal = self._adjust_viewport( self.viewport.horizontal_span, size.width, player_position.x, map.rect.horizontal_span, ) vertical = self._adjust_viewport( self.viewport.vertical_span, size.height, player_position.y, map.rect.vertical_span, ) self.viewport = Rectangle.from_spans(horizontal=horizontal, vertical=vertical) # viewport is from the pov of the map; negate it to get how much space # is added or removed around the map pad_left = -self.viewport.left pad_top = -self.viewport.top pad_right = (size.width - pad_left) - map_rect.width pad_bottom = (size.height - pad_top) - map_rect.height # TODO it's unclear when you're near the edge of the map, which i hate. # should either show a clear border past the map edge, or show some # kinda fade or whatever along a cut-off edge map_canvas = urwid.CompositeCanvas(CellCanvas(map)) map_canvas.pad_trim_left_right(pad_left, pad_right) map_canvas.pad_trim_top_bottom(pad_top, pad_bottom) return map_canvas
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 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