Hello, loves!

Today, unless I miss my guess, which frequently happens, we’ll see some of the strength of the YieldStepper notion. Tests FTW.

Our dungeons look much like we intend, with separate room sections connected by single paths between them:

map with five connected sections

However, we are working to add more capability for our Dungeon Design Team (I have the hat here somewhere). The purpose of those single paths is to allow us to bar entry to the next section until Dot has solved the problems of the current section, including whatever task opens the gateway to the next section. Furthermore, those paths need to have impermeable walls. Normally, in our dungeon building scheme, if two rooms share a wall, the builder code will place a passage in that wall, permitting, well, passage between them. This makes the dungeon much more interesting, with loops in the paths one can follow. But for these inter-section paths we cannot allow such incursions, because they would create shortcuts through which Dot might pass without solving all the puzzles we pose.

I have rough plans for all this. I think that if it were done “right”, it would go something like this:

  1. Isolate the code that connects two Suites with a path;
  2. During the build, identify each section as a Suite;
  3. Use the Suite-connection code to produce a path room;
  4. Mark the path room as impermeable.

As all this goes on, the build should record whatever information it needs for door-building, section-loading, whatever.

As things stand, our dungeon build is a function. If need be, it could be a method of a class, and quite likely it always should be. We’ll find out as we proceed.

If we were wise, which assumes facts not in evidence, we would do all this with tests. We’ll give ourselves the best possible chance by beginning with a test. Let’s try … write a build function that builds some rooms and makes a suite of them.

class TestAdvancedBuild:
    def test_hook(self):
        assert False

Fails. We have a new test class! Woot! So far so good. Now I’ll sketch a little build function, then see what it takes to make it work.

    def test_minimal_build(self):
        def steps():
            yield

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

This runs green. With the yield replaced by pass, we get this error:

    def run_to_completion(self, view=None):
>       for _ in self.steps:
E       TypeError: 'NoneType' object is not iterable

Ah, right. There has to be at least one yield, or Python doesn’t recognize step as iterable. Not an issue in our code.

Here’s a more meaningful test:

    def test_section_to_suite(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 steps():
            layout.subregion_LRBT(0, 20, 0, 20)
            diamond(25)
            diamond(25)
            layout.finish_map()
            rooms = layout.rooms
            assert len(rooms) == 2 or len(rooms) == 3
            suite = Suite(layout, rooms)
            yield

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

We build two small diamond rooms in the lower left quadrant. We connect them with finish_map after which there will be two rooms if they happened to be adjacent, and three if a path was needed to connect them. We grab the rooms and make a Suite.

So far so good. Let’s think about this.

Reflection

There are at least two issues that strike me. First, this method step is already pretty complicated. But we have an example of a function diamond, that can move some complexity out. So we’ll keep an eye on complexity but it doesn’t seem unsolvable.

Second, while it was easy to get the rooms for the first Suite, since it needs all the rooms we have, getting the next Suite’s rooms seems a bit more tricky. But can’t we just grab all the rooms and subtract the ones we just grabbed? Let’s try extending this test. No. Let’s write a new one, and try to make it better.

    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 steps():
            layout.subregion_LRBT(0, 20, 0, 20)
            diamond(25)
            diamond(25)
            layout.finish_map()
            rooms_1 = set(layout.rooms)
            assert len(rooms_1) == 2 or len(rooms_1) == 3
            suite_1 = Suite(layout, rooms_1)

            layout.subregion_LRBT(0, 20, 35, 55)
            diamond(25)
            diamond(25)
            layout.finish_map()
            rooms_2 = set(layout.rooms) - rooms_1
            assert len(rooms_2) == 2 or len(rooms_2) == 3
            suite_2 = Suite(layout, rooms_2)
            yield

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

We converted the rooms from layout to a set and subtracted. Nice enough. Won’t work quite like that for three, though.

I have an idea how to make those two patches of code alike enough to be used for all four of our corners. First this:

    def steps():
        previous_rooms = set()
        layout.subregion_LRBT(0, 20, 0, 20)
        diamond(25)
        diamond(25)
        layout.finish_map()
        all_rooms = set(layout.rooms)
        new_rooms = all_rooms - previous_rooms
        previous_rooms = all_rooms
        assert len(new_rooms) == 2 or len(new_rooms) == 3
        suite_1 = Suite(layout, new_rooms)

        layout.subregion_LRBT(0, 20, 35, 55)
        diamond(25)
        diamond(25)
        layout.finish_map()
        all_rooms = set(layout.rooms)
        new_rooms = all_rooms - previous_rooms
        previous_rooms = all_rooms
        assert len(new_rooms) == 2 or len(new_rooms) == 3
        suite_2 = Suite(layout, new_rooms)
        yield

That’s nearly good: we could just about extract those two bits as a method. But I think this will go better:

    def steps():
        layout.subregion_LRBT(0, 20, 0, 20)
        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
        suite_1 = Suite(layout, new_rooms)

        layout.subregion_LRBT(0, 20, 35, 55)
        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
        suite_2 = Suite(layout, new_rooms)
        yield

The previous version was accumulating previous_rooms, but we have no need to do that, since the layout does that as a matter of course.

Now we should be able to extract a function make_diamond_section. PyCharm does exactly what we had in mind:

    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()

The test continues to run green. Maybe we should move the assertion down into steps? No real need, this is just a test and example of how we might really do things.

I’ve only been at this for 80 minutes, but this is a super time to break and sum up. Commit: new tests demonstrating use of YieldStepper.

Summary

We see above that with YieldStepper we can put any code we want into the build steps. With the previous BuildStepper, we were much more constrained. So YieldStepper is showing its strengths already. Chuffed, I am, veritably happy as a dog with two tails.

I’m also pleased that I decided to do this in tests. I am often too lazy to do it, and when I do begin with tests, it usually goes better and never goes worse than trying to just jam the code in. It’s a shame that my habits aren’t better than they are. I do know better. And sometimes, like today, I actually do better.

It was much easier to build things up in small chunks, and easy to identify opportunities to remove duplication, which, as all here know, is my primary way of improving code. Doing something twice is a sign that there is a thing that can be isolated and just used instead of copied.

I want to call out the steps toward making the two sections more similar. I learned that trick around a quarter-century ago, when Kent Beck, working on the big screen, made two very different bits of code look alike, and then, when they looked alike, extracted them into a method. All looking on were amazed. Those were the days when we were all like gods, striding across the code, making it better. Or something.

But the point remains: the similarity was already evident, but by moving things around, and noticing that the previous values we needed were actually just the current set or rooms, we enabled the extraction that makes the code above more clear and rather nicely reusable. I’m sure we can extend this morning’s approach to making Suite instances for each section of our dungeon.

We’re still left with breaking out a Suite-to-Suite path maker, and making that path impermeable. But when we do that, and we will do that, assuming that I last that long, we’ll have the data we need in the build and we’ll just have a new method on layout that does the inter-section paths.

Unless I miss my guess, which is always a possibility. That’s what makes real programming fun.

And I’ll offer this bet: if you are currently condemned to using LLM-AI to build code, I’ll bet I’m having more fun than you are.

See you next time for more fun!