Hello, loves!

If things go as I expect, we’ll start working on the paths between sections of the dungeon. It could happen. It begins to happen.

My rough plan for the inter-section paths is to build suites for each end of the path, and then so call a layout method to draw the path. That method may not currently exist, but we know that the code to do the job is in there somewhere. Then, we’ll figure out a way to mark the path “impermeable”, that is, arrange things so that if a room tries to connect with it, it’ll be rejected. Frequent readers may recall that we have a structure that prevents rooms connecting more than once. I think we’ll see about extending that capability to prevent connecting even once.

For all these things, we’ll find out when we get there. And I plan to continue to work with tests to get this done. It always goes better and it went particularly nicely yesterday, so why not today?

Here’s our latest test on this topic:

    def test_two_sections_to_suites(self):
        def diamond(size):
            if origin := layout.unused_cell_or_none():
                room = RoomMaker(layout).diamond(number_of_cells=size, origin=origin)
                layout.add_room(room)

        def make_diamond_section() -> set[Any]:
            previous_rooms = set(layout.rooms)
            diamond(25)
            diamond(25)
            layout.finish_map()
            all_rooms = set(layout.rooms)
            new_rooms = all_rooms - previous_rooms
            assert len(new_rooms) == 2 or len(new_rooms) == 3
            return new_rooms

        def steps():
            layout.subregion_LRBT(0, 20, 0, 20)
            new_rooms = make_diamond_section()
            suite_1 = Suite(layout, new_rooms)

            layout.subregion_LRBT(0, 20, 35, 55)
            new_rooms = make_diamond_section()
            suite_2 = Suite(layout, new_rooms)
            yield

        layout = DungeonLayout(55, 55)
        stepper = YieldStepper(steps(), layout)
        stepper.run_to_completion()

A fanatic would have me write a new test to work on the path idea, but the test would look like this one and we’d have to move the helper functions around. We’re only going to work with steps for a while, so let’s sketch this in:

        def steps():
            layout.subregion_LRBT(0, 20, 0, 20)
            new_rooms = make_diamond_section()
            suite_1 = Suite(layout, new_rooms)

            layout.subregion_LRBT(0, 20, 35, 55)
            new_rooms = make_diamond_section()
            suite_2 = Suite(layout, new_rooms)

            path_room = layout.make_path(suite_1, suite_2)
            assert path_room is not None
            yield

There is no make_path, and I didn’t know what to assert about it. That’s OK, we’re just sketching. Now let’s see how paths are made. I think things start with finish_map in the layout:

    def finish_map(self):
        self.ensure_connected()
        self.make_passages()

    def ensure_connected(self):
        count = 0
        while not self.is_fully_connected() and count < 10:
            count += 1
            self._make_path_rooms()

    def _make_path_rooms(self):
        suites = self.define_suites()
        # self.check_intersection(suites)  # debug method
        # for s1, s2 in zip(suites, suites[1:] + suites[:1]):
        for s1, s2 in zip(suites, suites[1:]):
            cells = s1.find_path_cells(s2)
            if cells:
                room = Room(cells, 'path')
                self.add_room(room)
            # cells can be empty if no path is possible
            # because we only accept source room,
            # target room, or available cells.

I’m not surprised to find those last four lines, which do pretty much exactly what we need. Extract the method and return the room, I think. I try this:

    def _make_path_rooms(self):
        suites = self.define_suites()
        # self.check_intersection(suites)  # debug method
        # for s1, s2 in zip(suites, suites[1:] + suites[:1]):
        for s1, s2 in zip(suites, suites[1:]):
            self.make_path(s1, s2)
            # cells can be empty if no path is possible
            # because we only accept source room,
            # target room, or available cells.

    def make_path(self, s1: Suite, s2: Suite):
        cells = s1.find_path_cells(s2)
        if cells:
            room = Room(cells, 'path')
            self.add_room(room)
            return room
        else:
            return None

I should have moved those comment lines down. Do it now. A test is failing. I suppose it must be the current one. Ah. We need to clear the subregions before we look for the path. Subregions prohibit finding cells outside themselves.

        def steps():
            ...
            layout.clear_subregions()
            path_room = layout.make_path(suite_1, suite_2)
            assert path_room is not None
            yield

Test passes. Life is good. Commit: working on inter-section paths.

Well, life is semi-good. We need now to create the passages between this new room and the rooms in the sections to which it connects … and we do not know that. We may have made a misstep.

I think we’d have done well to look at find_path_cells in Suite. Let’s do so now:

class Suite:
    def find_path_cells(self, suite: Suite) -> list[Cell]:
        source, target = self.find_closest_pair(suite)
        rooms = [source.room, target.room]
        cells = self.layout.find_path(source, target, rooms)
        on_path = cells[1:-1]
        return on_path

Ah, yes. In this method, we find the closest two cells that might connect the suites. Then the find_path will return a path that includes those two cells. Those two cells are exactly the ones that our code wants to know about. I think we’re prefer to work with this information. We’ll change the test to call a new method in Suite, which we’ll extract first:

    def find_path_cells(self, suite: Suite) -> list[Cell]:
        cells = self.find_extended_path(suite)
        on_path = cells[1:-1]
        return on_path

    def find_extended_path(self, suite: Suite) -> Any:
        source, target = self.find_closest_pair(suite)
        rooms = [source.room, target.room]
        cells = self.layout.find_path(source, target, rooms)
        return cells

Now we’ll code inside our test for a while, which will make a bit of a mess, but enable us to see what we need and figure out where it goes.

    layout.clear_subregions()
    path_cells = suite_1.find_extended_path(suite_2)
    assert path_cells
    # now path_cells[0] is in one suite, and
    # path_cells[-1] in the other, and
    # path_cells[1:-1] are the cells we need for
    # the new path room.
    yield

The test is green. I wrote some notes as comments, since I needed to say those things here and want to remember them there. Excuse me a moment while I go over to Layout and remove the extra stuff from that method we extracted. I’ll leave it extracted, that was a righteous move in any case.

    def _make_path_rooms(self):
        suites = self.define_suites()
        # self.check_intersection(suites)  # debug method
        # for s1, s2 in zip(suites, suites[1:] + suites[:1]):
        for s1, s2 in zip(suites, suites[1:]):
            self.make_path(s1, s2)

    def make_path(self, s1: Suite, s2: Suite):
        cells = s1.find_path_cells(s2)
        if cells:
            room = Room(cells, 'path')
            self.add_room(room)
            # cells can be empty if no path is possible
            # because we only accept source room,
            # target room, or available cells.

Commit. We can always commit with green tests, and as we are kind of feeling our way, I’d prefer to commit often, as we inch forward, sort of like a ratchet jack stepping up one notch at a time.

Let’s Reflect

As things stand right now, we have a list of cells in our dungeon-building steps, not as high level an object as we might want to have. But it is an interesting one. It’s a list of cells. Its end cells are in rooms already, one from the first section, one from the second. The cells between those are in no room at present, but they are exactly the path cells we want in our new room, which we would like to have be immune to other rooms connecting.

From the viewpoint of the dungeon creator, this “object” is important. Somewhere along it we’ll want to place the “problem” that Dot has to solve to move to the next section. We could put that problem anywhere along the object, including in the two cells that are in the existing rooms.

At this moment, I don’t know what we need or how we’ll use it, but it seems clear that we will want to remember all this. It seems that we could remember it in the layout, or we could remember it right here in the build steps. I think it’s premature to put it into the layout. Let’s continue to build and see what happens.

Let’s do some asserts on what came back, and make a room of it. Then we’ll see if we can make it impermeable. I wonder how we could test that. Here we go for the room connecting part:

    layout.clear_subregions()
    path_cells = suite_1.find_extended_path(suite_2)
    # now path_cells[0] is in one suite, and
    # path_cells[-1] in the other, and
    # path_cells[1:-1] are the cells we need for
    # the new path room.

    room_2 = path_cells[0].room
    assert room_2 in suite_2.room_set
    room_1 = path_cells[-1].room
    assert room_1 in suite_1.room_set
    path_room_1_2 = Room(path_cells[1:-1])
    layout.add_room(path_room_1_2)
    layout.make_passages()

    assert path_cells[0].has_passage(path_cells[1])
    assert path_cells[-1].has_passage(path_cells[-2])
    yield

This test passes. I think it could fail if our two Suites turned out to be adjacent. Let me write a fake test for that.

Should we mark it to skip, or let it fail, or what? For now I’ll just leave this:

    def test_two_adjacent_sections(self):
        pass

We’ve done just about an hour of work and writing. Let’s have a break.

Summary So Far …

Our first step to connecting the section Suites turned out not to be in the right direction. We just belayed that one and took a different course, which seems to be working out so far. Given that the sections are not touching one another, we can draw a path between them, including the cells to be connected in the closest points between the suites. The inner cells of that path are the ones we want in our room connecting the cells. The outer two cells are the ones we connect to.

So we have all the facts we need to do our work, though we’re probably doing too much detailed work in the steps. When we have all the steps figured out, it should become more clear how to package them up for proper use.

It occurs to me that some decades back I’d have been terrified that we’re building up these things but don’t have any real idea how they’ll be used to allocate things in the dungeon, and so on and so on. I’ve come to be confident that while I will surely make a bit of a mess, I’m very unlikely to make a mess that can’t be readily cleaned up in small steps.

One might argue, yeah, but if you make 100 small steps out and then 100 small steps in, maybe you could have just made 100 small steps directly to what you needed, if you had just figured it out in advance. Or maybe only 50.

If I were more perfect than I am, maybe. But my experience with careful overall design is that to get it right takes a very long time, and then one has to build pretty much all of it before anything works, and then at the far end the design isn’t all that close to what you really needed anyway.

In short: my experience is that doing a lot of design before starting rarely, if ever, measures up to the dream of Design It Build The Design All Is Well.

But you do you, if you’re still allowed to do you. And if you’re not, well, it saddens me and I hope you’ll soon find a better situation.

We’ll just keep pushing the ball forward, bit by bit, in whatever direction feels right. We’ll see what happens, but it has worked well so far. I think that will continue.

And I’ll see you next time!