Hello, loves!

If we make the path-maker sensitive to active regions, would that solve our issues? It might, but we solve them without that. Most peculiar.

Note
Not much code is written below, but then I remove it all and discover that I don’t need it.

The issue we need to fix is that once we have populated three corners of the dungeon, we will have correctly connected those three: the first two have no choice but to connect, and two and three will connect correctly because #2 is closer to #3 than #1 is. But when we go to connect up corner #4, it is a toss-up whether #1 is closer to #4, or #3 is. #2 is right out.

My first idea, as mentioned in the blurb, is to make the path making sensitive to region. If, before connecting #3 and #4, we set a right-side region, and only select rooms that fall in that region, it should do the right thing. Let’s see how we might do that:

class DungeonLayout:
    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)

The commented-out for is the one that links back to front, always resulting in a loop among the rooms connected. We don’t want that all the time, so I’ve just turned it off for now at least.

In the code above we add rooms that connect separate “suites” together. We do not define the actual passages until later, in make_passages. I am hopeful that we can leave make_passages alone, at least for now.

If define_suites only found the right suites, I think we’d likely be OK. So …

class DungeonLayout:
    def define_suites(self):
        suites: list[Suite] = []
        unexplored = self.rooms.copy()
        while unexplored:
            suite: Suite = self.find_suite(unexplored[0])
            suites.append(suite)
            unexplored = [room for room in unexplored if room not in suite.room_set]
        return suites

If we only considered the current region as we set up unexplored, we should get the right result. Let’s try Wishful Thinking:

    def define_suites(self):
        suites: list[Suite] = []
        unexplored = self.rooms_in_subregion()
        while unexplored:
            suite: Suite = self.find_suite(unexplored[0])
            suites.append(suite)
            unexplored = [room for room in unexplored if room not in suite.room_set]
        return suites

Now we just have to write that method:

class DungeonLayout:
    def rooms_in_subregion(self):
        return [room for room in self.rooms if self.subregion.contains_room(room)]

So far so good. Now:

class SubRegion:
    def contains_room(self, room):
        for cell in room:
            if cell not in self:
                return False
        return True

That looks right. Ask me where my tests are, but not until we try this.

Lost Track

Somewhere between that last sentence and this morning, I was just fiddling, and finally realized that my subregions weren’t quite right, especially at the end. Even with all the Wishful Thinking code removed, and the right sequence of subregion setup, we’re getting maps that are what we intend, such as this one:

five section map with paths circling from bottom left ending at central room

That’s really what I was trying to do, and the glitches I was seeing had to do with improperly setting up the subregions, making the code fail to discover space for the paths needed.

Running a few more random dungeons, I do get this map:

map with path from 3 to 4 and 4 to central merged

I’m not sure whether to consider that a defect or not. If we put the door to the central boss room at the entrance to the room, this would be perfectly valid. Even if we put the door at the beginning of the stub path from the vertical hallway, it would be valid, and I’m rather sure that’s where the stub path goes.

Fiddling with the subregions generated some maps that weren’t acceptable but also this very fine one:

very intricate and attractive map

I think we’re pleased with our results here. Setting the subregions back to the ones that created the first two maps above, we’ll commit the changes. As it turns out, after some reasoning errors on my part, all we seem to need is careful selection of subregions and stepper flow. The stepper looks like this:

def make_build_table(layout, dungeon):
    steps =  [
        bottom_left,
        make_a_diamond_room,
        make_a_diamond_room,
        make_a_cave_room,
        make_a_diamond_room,
        make_a_diamond_room,
        finish,

        top_left,
        make_a_round_room,
        make_a_round_room,
        make_a_round_room,
        make_a_round_room,
        make_a_round_room,
        finish,
        clear_subregions,
        finish,

        top_right,
        make_a_cave_room,
        make_a_diamond_room,
        make_a_cave_room,
        make_a_cave_room,
        make_a_round_room,
        finish,
        clear_subregions,
        finish,

        bottom_right,
        make_a_cave_room,
        make_a_diamond_room,
        make_a_cave_room,
        make_a_diamond_room,
        make_a_cave_room,
        finish,
        right_side,
        finish,

        clear_subregions,
        make_diamond_in_round_room,
        bottom_right_plus_center,
        finish,
        clear_subregions,

        populate,
        add_content,
    ]
    return BuildStepper(steps, layout, dungeon)

Let’s sum up.

Summary

The trick to getting two sections to connect is to make the subregion, draw the section, finish to make sure it is connected, then clear the subregions and finish again, which will connect everything that isn’t connected. You can exert sufficient control what connects by setting the subregions properly.

With my product manager hat on (a very jaunty fedora, I might add) this scheme seems to be doing what I asked for, so far, and I suspect that we’re going to want better control over the layout. Discussing this with the lead developer (he doesn’t wear a hat at all and his hair is a mess) we agree to wait and see, and I’m comfortable that we’ll be able to improve the situation should we need to.

In the actual world of ronjeffries.com, we’re just here to set ourselves interesting improvements to the program that tend to break the design, so as to discover that we can improve it without massive rewriting delays, which have, in my development lifetime, killed more than one project that didn’t deserve to die.

What is interesting about this problem, which is to create a dungeon in sections that could be separately populated with problems and solutions, is that while I did try a lot of ideas to get this to happen, so far most of the work has been in configuring subregions that will cause path creation to do what we want.

We do still have one or two things to do, and some new ideas have come to mind:

  1. We need to provide some way to control “doors”, or whatever kind of obstacles the dungeon designer wants to place between sections. This will require more feedback between the build sequence and the designer, some kind of trace of where key locations are, perhaps.

  2. We probably want to create our inter-section paths to be impermeable, so that they don’t get openings other than at the ends, which can lead to shortcuts that the designer did not contemplate.

  3. We might want to provide a more robust stepper “language” that gives more control to the dungeon designer.

  4. The Suite object came into view a couple of times during this exercise, and it is possible that we’d benefit by making it more explicit and capable. For example, if a subregion’s rooms automatically formed a Suite, we might have better control over how paths are created.

  5. The current paths draw are quite straight. We have the ability to make them more wandering, and might want to use that, or even enhance it.

  6. … We’ll run out of interest in this program long before we run out of ideas. I fully intend to be working on something different by 2027, assuming that I last that long.

Summary Summary

Small changes leading from where we are to where we need to be seem always to be possible. Of course there are exceptions. I might not be able to figure out a small step transition from Python on the Mac to COBOL on a mainframe. Oh, wait, we already figured out how to go from COBOL on the mainframe to Smalltalk on a SparcStation in small steps. Maybe something similar … but no one wants a dungeon in COBOL anyway …

Small steps, my friends, seem always to be possible. They seem to be easier to make, less prone to errors, and more flexible with respect to interweaving other features.

Fascinating. See you next time!