We’re working on finding a cell that is maximally distant from a given cell. What are our next steps?

My work yesterday was somewhat interrupted by local matters, so I have even less recollection than usual about what was done and what should be next. I didn’t even leave a non-running test as a reminder. Fortunately, I did append yesterday’s tests to the test class, so those should give a clue. They included:

    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....
        '''
    ...

    def test_rooms_from_dictionary(self):
        dict = { '0': [(0,0), (0,1)],
                 '1': [(2,0)],
                 '2': [(4,0), (4,1), (4,2)]}
        ...

The first test converts our little string drawing to a dictionary from a one-character room name—’0’, ‘1’, ‘2’ in the test—to a list of the x y tuples where that character shows up in the picture. The second test test-drove a method on DungeonLayout, add_from_dictionary, which converts that dictionary to rooms containing the indicated cells and adds them to the dungeon.

If we can put those two things together, we’ll be able to draw sample dungeons with simple text pictures that can reside in the code, and then use those dungeons to test things. The first thing we’ll test, I’m supposing, will be the new feature we want, finding a cell that is as far away from a given cell as possible, through the rooms and paths that exist in the dungeon. The longest direct walk possible from the given point, in other words.

The string to dictionary code currently lives only in the test and we need to decide how to make it reusable. Let’s take a look at the whole test now:

    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 char != '.':
                    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

I don’t think I like that code much, so we’ll see about making it a bit better, and a larger question is where it should reside. I am inclined to think that it should go on DungeonLayout along with the add_from_dictionary method. If we’re going to have the one, we should have the other.

Let’s do that, moving it as it is, via a cut, wishful thinking, and a bit of pastery. You know what? An extract will help us here:

    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....
        '''
        room_cells = self.string_map_to_dictionary(string_map)
        assert len(room_cells) == 3
        assert room_cells['0'] == r_0
        assert room_cells['1'] == r_1
        assert room_cells['2'] == r_2

    def string_map_to_dictionary(self, string_map):
        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 != '.':
                    if not char in room_cells:
                        room_cells[char] = []
                    room_cells[char].append((x, y))
        return room_cells

Now we have a method which could readily reside on DungeonLayout, so let’s do the wishful thinking part, where we write the code in the test that we wish would work:

    layout = DungeonLayout()
    room_cells = layout.string_map_to_dictionary(string_map)

Test goes red. We just have to move that method. PyCharm can’t help, so I do it the old-fashioned cut and paste way:

class DungeonLayout:
    @staticmethod
    def string_map_to_dictionary(string_map):
        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 != '.':
                    if not char in room_cells:
                        room_cells[char] = []
                    room_cells[char].append((x, y))
        return room_cells

Tests are green. Commit: add string_map_to_dictionary to DungeonLayout.

Curiously, having the code over there in DungeonLayout makes me a bit more comfortable with the fact that I don’t like it much and feel it’s probably not robust. At least it’s in the right place, where we can find it and fix it when we want to, or when we find trouble.

Now we need one more method on DungeonLayout, the one that takes a string map and adds the corresponding rooms, using these two new methods. We can gin that up from our existing tests. I think we’ll leave the two we have and create a new one, since the two test the individual methods nicely.

    def test_add_rooms_from_map(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....
        '''
        layout = DungeonLayout()
        layout.add_rooms_from_map(string_map)
        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

This takes the top from the first test and the bottom from the second, and assumes the method add_rooms_from_map, which doesn’t exist yet. It should be easy:

    def add_rooms_from_map(self, string_map):
        d = self.string_map_to_dictionary(string_map)
        self.add_from_dictionary(d)

I am somewhat surprised to find that that doesn’t work. Why not?

test_paths.py:137 (TestPaths.test_add_rooms_from_map)
(4, 1) != [(4, 0)]

Expected :[(4, 0)]
Actual   :(4, 1)

Silly me, I gave it the wrong answer. Improve the test:

    def test_add_rooms_from_map(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....
        '''
        layout = DungeonLayout()
        layout.add_rooms_from_map(string_map)
        room_0_cells = layout.cells_in_room('0')
        tuples_2 = [c.xy for c in room_0_cells]
        assert set(tuples_2) == r_0
        room_1_cells = layout.cells_in_room('1')
        tuples_1 = [c.xy for c in room_1_cells]
        assert set(tuples_1) == r_1
        room_0_cells = layout.cells_in_room('0')
        tuples_0 = [c.xy for c in room_0_cells]
        assert set(tuples_0) == r_0

Now we’re checking each of the rooms and they are correct. Test green the feature works. Commit: add_rooms_from_map

I want to see this in the visible map, so I’ll hack main briefly, and we get this:

screen map looking like the string map

That’s just right. We even put Dot in one of the rooms. We’ll call it a morning after these few lines of code, and take a well-deserved break.

In all seriousness
I would generally recommend that even when working for a living, one might take a little break, stretch one’s legs, check out the coffee room, pet the cat, after reaching a milestone or inch-pebble like this one. Kind of bring things back to a pause before starting the next effort. And, if you’re as lucky as I am, you have a few hundred lines of text and some code to talk about, so it is time for a well-deserved iced chai or beverage of your choice.

Summary

Yesterday we had the desire to turn a text picture into rooms, and we had a test that did part of the job longhand in the test, and a test that had driven the other half of the job over to DungeonLayout. Today, we just moved the longhand code over to the Layout class, and then test-drove a simple method that called the two chunks to get what we wanted.

I can imagine having done the whole thing in one big messy test and one big messy method and then, perhaps, refactoring. But as I wrote yesterday’s first test, it became clear that going all the way from string to tuples to cells to rooms was too big a jump for my feeble brain, so I stopped with the tuples and then processed the tuples.

I kind of have the feeling that working from the tuples may come in handy in the future, since we have a lot of tests that talk about Cell.at this and Cell.at that. Even if not, it is a sensible breakdown of the job of converting the map to rooms, and I expect the map to come in handy quite soon.

I’m not fond of the method names, as they are long and mention “data types”, which is a bit naff, so we may find ourselves coming up with better names as we go forward.

The whole process today would have gone perfectly had I not whiffed the test, plugging in a wrong answer. The code was correct and the test was quite wrong. That should tell me something. Probably that the tests are still too hard to write, and that I was probably trying to go too fast and wasn’t paying attention. Anyway it was just a momentary bump in the road.

We didn’t do as many commits as I’d like to have seen, only a couple yesterday and a couple today. There were probably some save points in there where I could have notched a commit in, but since we didn’t ever need to back up, I got away with it. It’s not a great way to bet, since even with very small steps, sometimes I do have to back up and a long revert is always a pain. So notching every bit of progress with a commit seems to be the best way for me.

Anyway, a nice new feature for our “making app” and we’ll start using it next time as we work on the furthest point idea. See you then!