Hello, loves!

Stepping is working. Let’s make it more nearly right. I think an object is called for. Tout le monde déteste l’IA.

What, you may wonder, makes me think that we need an object? There are a few clues, and one of them is quite strong. Here’s the build_table as created in ‘main’:

def make_build_table():
    return [
        make_diamond_in_round_room,
    make_a_diamond_room,
    make_a_round_room,
    make_random_rooms,
    finish,
    populate,
    add_content,
    ]

This is just a list of functions, intended to be called with a Layout and a Dungeon. One clue (1) is here: this list is quite specialized inside, and yet it is just a simple list. Very special contents often suggests a special object to contain the very special contents.

Here is the code, in KeyPress, fetching the list and initializing to use it:

class KeyPress:
    def __init__(self, view, dungeon, pub_sub):
        from main import make_build_table # <===
        self.view = view
        self.dungeon = dungeon
        self.pub_sub = pub_sub
        self.key_lock = None
        self.build_table = make_build_table() # <===
        self.build_item = 0 # <===

There are two clues here: (2), the need to put that import inside the __init__ tells us that there is probably some kind of recursive import going on if we don’t do that. A specialized object, properly provided, might help with that. In addition, (3) while the top three instance variables have to do with the general operation of KeyPress, the last two are special for using the build table.

And here’s the code for when you type the magic character, presently D, to step the list:

class KeyPress:
    def on_key_press(self, symbol: int, modifiers: int):
        ...
        elif symbol == arcade.key.D:
            from dungeon_view_maker import DungeonViewMaker
            if self.build_item >= len(self.build_table): return
            action = self.build_table[self.build_item]
            self.build_item += 1
            action(self.dungeon, self.dungeon.layout)
            maker = DungeonViewMaker(self.dungeon)
            maker.update(self.view)
            if self.build_item == len(self.build_table):
                self.dungeon.run()

Clues here include, (4), another embedded import. Finally (5), and this is the big one, the code here in KeyPress isn’t very concerned about KeyPress matters at all. It’s all concerned about that table and count, plus a DungeonViewMaker, which it creates. This code is begging for some help, begging for an object that does this work that isn’t about keystrokes at all.

All these clues induce me to build an object, which I think we’ll call BuildStepper, at least for now. And I think I’d like to test-drive it.

Make a new test file.

class TestBuildStepper:
    def test_create(self):
        assert False

Fails as intended. I do the above, by rote, to ensure that the testing framework has found the new test. In recent months it always has but I retain the habit. Can’t hurt, might help.

Now more of a test:

    def test_create(self):
        table = [ f_1, f_2 ]
        result = []
        stepper = BuildStepper(table, result, None)
        assert result == []
        stepper.step()
        assert result == [1]
        stepper.step()
        assert result == [1, 2]

My plan is that f_1 and f_2 will append 1 and 2, respectively, to the input layout. Fake action but it’ll show that the calls happened. So:

def f_1(layout, dungeon):
    layout.append(1)

def f_2(layout, dungeon):
    layout.append(2)

And a small class, so far:

    def __init__(self, table, layout, dungeon ):
        self.table = table
        self.index = 0
        self.layout = layout
        self.dungeon = dungeon

    def step(self):
        self.table[self.index](self.layout, self.dungeon)
        self.index += 1

We need to prevent running off the end of the list. And, I’ll speculate a bit here and guess that we would like to know, upon calling step, whether there are more steps left.

I’ll write a new test for both those conditions.

    def test_limits(self):
        table = [ f_1, f_2 ]
        result = []
        stepper = BuildStepper(table, result, None)
        assert result == []
        more = stepper.step()
        assert more is True
        assert result == [1]
        more = stepper.step()
        assert more is False
        assert result == [1, 2]
        more = stepper.step()
        assert more is False
        assert result == [1, 2]
class BuildStepper:
    def step(self):
        if self.index >= len(self.table): return False
        self.table[self.index](self.layout, self.dungeon)
        self.index += 1
        return self.index < len(self.table)

Green, but I don’t really like the structure. How about this:

    def step(self):
        if self.index < len(self.table):
            self.table[self.index](self.layout, self.dungeon)
            self.index += 1
        return self.index < len(self.table)

That’s more pleasant. Let’s extract a private method though:

    def step(self):
        if self._more_to_do():
            self.table[self.index](self.layout, self.dungeon)
            self.index += 1
        return self._more_to_do()

    def _more_to_do(self):
        return self.index < len(self.table)

Just for fun, let’s extract again:

    def step(self):
        if self._more_to_do():
            self._do_one_step()
        return self._more_to_do()

    def _more_to_do(self):
        return self.index < len(self.table)

    def _do_one_step(self):
        self.table[self.index](self.layout, self.dungeon)
        self.index += 1

That’s how you do that. (As one cat said to another after they saw me fall off the roof and land on my feet.)

Reflection

I’m glad I did this bit of TDD. However, I’m not entirely sure that this is the object we really want. There’s code in the D key press that this object can help with but there’s code that it cannot.

I imagine that we could think our way to the bottom of this but let’s just install the class and see what comes up.

Commit. Move the class from inside the test, where I commonly create new tests, to the ‘src’ tree as ‘build_stepper.py’. Commit.

Let’s have the main program create the build stepper.

Reflection

This took longer than I’d have thought reasonable. The issue turned out to be that just about everyone who receives a layout and a dungeon gets the layout parameter before the dungeon, but the build functions in main have dungeon first. That took me a while to recognize.

Since a dungeon has a layout, we could argue that we shouldn’t pass both anyway. That would, however, lock us in on dungeon having a layout, because other objects need access to it. Perhaps we shouldn’t bless that relationship.

Installing the BuildStepper was a bit intricate, and it comes down to this:

class KeyPress:
        elif symbol == arcade.key.D:
            more = self.stepper.step()
            maker = DungeonViewMaker(self.dungeon)
            maker.update(self.view)
            if not more:
                self.dungeon.run()

So the code in KeyPress is somewhat better and because we don’t need the other import in the init, the DungeonViewMaker import can be at the top, so that issue is resolved.

But it still seems to me that the DungeonMakerView thing shouldn’t be so visible here. It’s weird in any case, because we create a new one and then clone its results into the real view.

I’d like the above code a bit better if it were able to check before running, whether there was more to do. So maybe we need to make that _more_to_do method public. Another issue is that once our D command discovers that the stepper cannot run, it should return rather than do dungeon.run again and again every time someone types D.

I think we’ll declare a rest period and improve things later. The stepper is working as intended. We’ll review the setup at that time as well.

For now, commit and go make a nice iced chai. See you next time!