Hello, loves!

An upcoming feature demands a capability we don’t have. Let’s improve the Making App a bit. A good first step.

GeePaw Hill coined the Shipping App / Making App terminology. In our context here, pace Hill, we’ll use the terms to refer to the features of the program that are part of the game, versus those that are for our use in building, testing, and debugging the program.

We’re about to undertake populating the dungeon with puzzles and treasures and such. A very simple puzzle will serve as an example of why we need some additional tooling on the Maker side. Suppose some part of the dungeon is closed off by a door, so that the passage between one room and the next can only be traversed if the door is unlocked, using a key which can be found somewhere in the dungeon. We need to be sure that we place the key on the same side of the door as we place Dot, since otherwise she could never find it.

To ascertain whether Dot can reach the key, we need to determine that there is a path starting from Dot that reaches the key, or vice versa. We do that by a flood search of the dungeon, starting with one end point, looking for the other. In the code we have an object, Flooder, which we use for purposes like that. As we’ll see, it is fairly powerful and has an interesting “fluent” interface. As we’ll also see, it is not adequate as it stands, because it does not understand about walls and passages.

Our near-term mission is to enhance Flooder to respect walls and passage when we want it to.

Testing Flooder is rather difficult, since it acts as a generator of cells in search order, and since the search expands from the source, it generates mass quantities of cells. So I want to see it working.

Our even nearer-term plan is to visualize the result of a Flooder run. The rough plan is this:

Add a key-stroke-command, ‘F’, to run a Flooder command that we’ll provide as part of the F code. The Flooder will return the manhattan distance of the discovered cells from the source. We’ll have Dot’s location be the source. And as it generates cells, we’ll display that distance in the cell in question. The result will be that all the cells around Dot, and all the cells reached, will light up with their travel distance from Dot.

With that display in hand, we’ll be able to see, first, that Flooder does not honor walls and passages, and then, we’ll add what we need to cause it to honor them. So we’ll have visual confirmation of what we’re doing.

We’ll try to write tests for this, but I am not optimistic about that, and not very motivated. For me, this is the kind of tooling that is best supported by a lot of looking. But we’ll see how it goes.

I think our nearest-term plan will be to add a data structure to DungeonLayout, mapping cell to a number, and to make it display the number in the corresponding cell on the map.

Let’s spike a bit, but with the usual caveat that if this goes well enough we’ll evolve it forward rather than throw it away. Yes, I know the rule about always throwing away Spikes. I just don’t always follow it, because my rule is incremental development, and a spike is often a perfectly good first step, except when it isn’t.

We’ll begin in DungeonView, because that is where we see keystrokes and do our drawing:

class DungeonView:
    def on_key_press(self, symbol: int, modifiers: int) -> bool | None:
        if self.key_lock is not None:
            return
        self.key_lock = symbol
        if symbol == arcade.key.RIGHT:
            self.dungeon.move_player(Direction.EAST)
        elif symbol == arcade.key.LEFT:
            self.dungeon.move_player(Direction.WEST)
        elif symbol == arcade.key.UP:
            self.dungeon.move_player(Direction.NORTH)
        elif symbol == arcade.key.DOWN:
            self.dungeon.move_player(Direction.SOUTH)
        elif symbol == arcade.key.F:
            self.dungeon.maker_flood()
        ...

I’m not sure whether this should go into Dungeon or DungeonLayout. I choose Dungeon, because it knows the layout and the layout doesn’t know the dungeon, so we’re more likely to be able to find what we want.

My plan, very tentative, is to have a dictionary in Dungeon, from cell to integer, and to have DungeonView unconditionally draw it. So we’ll fake one in for now.

class Dungeon:
    def maker_flood(self):
        dot = self.player_cell
        n = dot.offset_by((0,1))
        e = dot.offset_by((1,0))
        w = dot.offset_by((-1,0))
        s = dot.offset_by((0,-1))
        self.flood_dictionary = {dot:'!', n: 'n', s:'s', e:'e', w:'w'}

You can see that I decided to make ti a dictionary from cell to string, at least for now.

Now we need to draw that, back in the View:

class DungeonView:
    def on_draw(self):
        self.clear()
        self.scroll_dungeon_camera()
        self.illuminate()
        with self.dungeon_camera.activate():
            self.room_sprite_list.draw()
            self.draw_contents()
            self.draw_passages()
            self.draw_flood()
        with self.scroller_camera.activate():
            self.scroller.draw()

I plan to do this with simple test drawing once I relearn how to do it.

    def draw_flood(self):
        for cell, string in self.dungeon.flood_dictionary.items():
            cx, cy = cell.center_position(cell_size)
            arcade.draw_text(text=string, anchor_x="center", anchor_y="center",x=cx, y=cy)

When I type F at game startup, we get this:

map showing Dot surrounded by n e w s

That’s nearly good. Text is too big, we need room for at least 2 preferably 3 digits. We’ll try size 8. This is a good start, commit: beginning work on flood viewing assistance.

I think I’ll just fire off a real Flooder call in there, showing distance. I’ll change the display code to do a str on everything.

    def maker_flood(self):
        def further(c, n):
            return n + 1
        for cell, distance in (Flooder(layout=self.layout, origin=self.player_cell)
                .in_any_room()
                .initial_value(0)
                .next_value(further)
                .flood()):
            self.flood_dictionary[cell] = str(distance)

That flood starts at Dot (player_cell) and returns the distance from Dot to the cell being produced. The further function is called before each cell is generated, given the value from the parent cell, so it computes manhattan distance from the origin. And it works, with a bit of a caveat:

map showing manhattan distance from Dot

Those are the correct distances. We can see our current concern if we scan right from Dot, where we see that the distance to the lower left corner of the room off to her right is shown as ‘5’, because we’re searching for cells in_a_room, and thus the flood is ignoring walls. That’s what we are planning to improve.

But the caveat is that displaying all those numbers is killing the performance of the scroller, almost certainly because displaying all those numbers is taking much longer than a normal draw cycle. For our purposes that’s not a deal-breaker, we can always turn it off or wait until it goes away. But we can do at least one little thing, which will be to to clear the dictionary if it is presently full.

    def maker_flood(self):
        def further(c, n):
            return n + 1
        if len(self.flood_dictionary) > 0:
            self.flood_dictionary = dict()
            return
        for cell, distance in (Flooder(layout=self.layout, origin=self.player_cell)
                .in_any_room()
                .initial_value(0)
                .next_value(further)
                .flood()):
            self.flood_dictionary[cell] = str(distance)

Now the F key toggles. If the flood dictionary is already full, we empty it and return. Otherwise we fill it.

We can commit this: F key runs the Flooder command in maker_flood.

I think we’ll pause here: it’s a decent foothold.

Summary

In a few lines, we have added a keyboard command F that toggles the result of a Flooder run on and off, rerunning the flooder every other time we hit F, removing the display on the next F. Strictly speaking, it empties the data structure, which is unconditionally displayed.

The example Flooder in there now demonstrates the concern, which is that the Flooder currently has no way to restrict flooding to passages when moving between rooms. The current selection criteria just can’t express that, as we’ll see next time. Our next step is to improve Flooder to honor walls and passages … and then we’ll need to do something about passages that may or may not be open. (Or maybe they just aren’t passages unless they are open. We’ll see what we like when we get there.)

I expect this simple too to be useful for the current need, floods that honor passages, but that’s not likely to be the last time we find it to be of value. For example, we’ll probably want some Dungeon Denizens to move to where Dot is, or to strive to move away from her. We might even have some kind of magical item that senses treasures or dangers and indicates where they are. But even if it’s just useful this once, the investment has been quite small and the value will be high.

I am getting an idea about tests from looking at the display as well. We could build a little two-room dungeon. Think two adjacent squares with a single passage at the top of the wall dividing them. We could check to see if the distance from a cell at the bottom of square 1 is the right distance from a similar cell in square 2. Up and over, where now it would just scan right through the wall.

Would I have thought of that anyway? Probably. Who knows? I know that seeing a vertical wall with the numbers going right through it triggered the idea this time.

Visualizing things has great value. It is flawed, as our eyes are designed to deceive us in some regards, so as to serve us in others, and our brains, well, whoever designed those made some interesting decisions. So we won’t forget testing, but we will let the eyes and brain help in every way they reasonably can.

A good first step. See you next time!