Sometimes we focus on the application, sometimes on the making of the application. We begin with making.

My brother Hill refers to the “App” and the “Making App”, that is, the code that is actually part of the application, vs the code that is part of making the application. Today I have a feature in mind, which drives me to improve my tool set.

The feature that I want is the ability to find a cell in the map which is maximally distant from a given cell, by Manhattan measurement. We might extend this to finding a cell which is at least some given distance, but the current plan is to find a maximally distant call.

I have a tentative plan for this, which will involve using our expanding cell search to collect some distance information along the paths searched, details to be figured out soon.

Clearly we’ll want to test-drive this, as it is somewhat tricky and anyway that is the style in which we prefer to work. That leads to a need for some improvement to how we build tests.

Yesterday I drew this little map, to test the ensure_connection:

..0.2
..0..
..000
.....
1....

It took me three or four tries to correctly convert that picture to the correct cell coordinates to create the rooms. I kept mixing up x and y, and probably made other random errors. Even when we do get it right, reading the test doesn’t tell us what the map looks like:

    def test_connected(self):
        layout = DungeonLayout()
        Cell.create_space(5, 5)
        r_0 = Room([Cell.at(2,0), Cell.at(2, 1), Cell.at(2,2),
               Cell.at(3,2), Cell.at(4,2)])
        layout.add_room(r_0)
        layout.add_room(Room([Cell.at(0,4)]))
        layout.add_room(Room([Cell.at(4, 0)]))
        suites = layout.define_suites()
        assert not layout.is_fully_connected
        layout.ensure_connected()

So, to improve our ability to build little maps for testing, I propose to build the ability to turn a multi-line string like the one above into a DungeonLayout. I assume that Python has multi-line strings and some way to take them apart.

Let’s write a test, using the layouts above.

Note
Another inspection showed me that the code for test_connected was a valid test but did not reflect the drawing. I corrected the lines above and I’m pretty sure I got it right this time.

After some fiddling, I have this longhand test, producing collections of x,y tuples:

    def test_string_layout(self):
        r_0 = [(2,0), (2, 1), (2,2),
               (3,2), (4,2)]
        r_1 = [(0,4)]
        r_2 = [(4, 0)]
        string_map = '''
            ..0.2
            ..0..
            ..000
            .....
            1....
        '''
        string_map = textwrap.dedent(string_map)
        strings = string_map.split('\n')
        strings = [ string for string in strings if string]
        room_cells = dict()
        for y, line in enumerate(strings):
            for x, char in enumerate(line):
                if not char in room_cells:
                    room_cells[char] = []
                room_cells[char].append((x, y))
        assert room_cells['0'] == r_0
        assert room_cells['1'] == r_1
        assert room_cells['2'] == r_2
Note
In writing what follows, I realize that the room_cells dictionary includes too much. We’ll fix that but let me explain what we almost have so far.

The rigmarole with the text dedent removes any equal indentation, the split gives us lines of text, and the list comprehension removes any empty lines. There will be one at the beginning.

Then, what I was trying to do was go through and record the x,y locations of any non-dot cells, but I forgot to skip the dot.

        string_map = textwrap.dedent(string_map)
        strings = string_map.split('\n')
        strings = [ string for string in strings if string]
        room_cells = dict()
        for y, line in enumerate(strings):
            for x, char in enumerate(line):
                if char not in ['.']:
                    if not char in room_cells:
                        room_cells[char] = []
                    room_cells[char].append((x, y))
        assert len(room_cells) == 3
        assert room_cells['0'] == r_0
        assert room_cells['1'] == r_1
        assert room_cells['2'] == r_2

The test fails 4 != 3 without the new if. I used in because I think we might allow other than .. That’s premature. generalization Change it:

    for y, line in enumerate(strings):
        for x, char in enumerate(line):
            if char != '.':
                if not char in room_cells:
                    room_cells[char] = []
                room_cells[char].append((x, y))

I think that what we need is a method on DungeonLayout to do this job and actually create the Room instances.

And bah: we need a test for that as well. We have no decent way to test a whole layout for correctness. Let’s see what might make sense. We have code here that makes a dictionary of tuples, given a specialized string. That has no reason to live anywhere, really.

We do have the class RoomMaker, which is a sort of eclectic collection of ways to make individual rooms of various shapes, round, diamond, cave, and so on. Whatever we’re trying to do might fit there. Or we might just put something in DungeonLayout to accept a dictionary of x,y tuples and make rooms from it.

The hole is getting deeper but I think we might be nearing the bottom. Let’s try a test for add_from_dictionary.

This seems close, and enough to drive out the method and find out what’s wrong with this idea:

    def test_rooms_from_dictionary(self):
        dict = { '0': [(0,0), (0,1)],
                 '1': [(2,0)],
                 '2': [(4,0), (4,1), (4,2)]}
        Cell.create_space(5, 5)
        layout = DungeonLayout()
        layout.add_from_dictionary(dict)
        room_2 = [room for room in layout.rooms if room.name == '2'][0]
        cells = [c.xy for c in room_2.cells]
        assert (4,0) in cells

We code this:

DungeonLayout:
    def add_from_dictionary(self, name_to_xy):
        for name, tuples in name_to_xy.items():
            cells = [ Cell.at(x,y) for x,y in tuples ]
            self.add_room(Room(cells, name))

And the test is green. Let’s continue making the test a bit more robust:

    def test_rooms_from_dictionary(self):
        dict = { '0': [(0,0), (0,1)],
                 '1': [(2,0)],
                 '2': [(4,0), (4,1), (4,2)]}
        Cell.create_space(5, 5)
        layout = DungeonLayout()
        layout.add_from_dictionary(dict)
        room_2 = [room for room in layout.rooms if room.name == '2'][0]
        cells_2 = [c.xy for c in room_2.cells]
        assert (4,0) in cells_2
        assert (4,1) in cells_2
        assert (4,2) in cells_2
        room_1 = [room for room in layout.rooms if room.name == '1'][0]
        cells_1 = [c.xy for c in room_1.cells]
        assert (2,0) in cells_1

Green. Commit: working on layout from strings.

I think that the room comprehension above could be a method on layout. Change the test to demand it: “wishful thinking”.

    def test_rooms_from_dictionary(self):
        dict = { '0': [(0,0), (0,1)],
                 '1': [(2,0)],
                 '2': [(4,0), (4,1), (4,2)]}
        Cell.create_space(5, 5)
        layout = DungeonLayout()
        layout.add_from_dictionary(dict)
        room_2_cells = layout.cells_in_room('2')
        tuples_2 = [c.xy for c in room_2_cells]
        assert (4,0) in tuples_2
        assert (4,1) in tuples_2
        assert (4,2) in tuples_2
        room_1_cells = layout.cells_in_room('1')
        tuples_1 = [c.xy for c in room_1_cells]
        assert (2,0) in tuples_1

And:

DungeonLayout:
    def cells_in_room(self, room_name):
        room = next((room for room in self.rooms if room.name == room_name), None)
        if room:
            return room.cells
        else:
            return []

Green. Commit: adding cells_in_room(name) and add_from_dictionary

I need a break. Let’s sum up and continue later.

Summary

We’re still improving the “making app”. We have two useful new methods on DungeonLayout, add_from_dictionary and cells_in_room(name), both of which are only used in tests. I have a feeling that both those methods will turn out to be useful in production, but even if not, they’re useful already in that they make it easier to write tests and check results. If we were tight on memory, very unlikely these days, we might add a test-only subclass of DungeonLayout to hold them or something like that, but really there is no need for anything that fancy.

Looking forward, I think we will find a place, probably Layout, for our code that converts a string map to rooms, and of course we’ll test that into existence. Then, we can turn our attention to the feature we want, which will be tested by creating a layout that we understand and finding a point of maximal distance from a given point.

So far, it’s going well. There are distractions here at home, so for now, I’m calling a break.

See you next time!