Let’s see what’s up with that new generator. I think it’s about 0.75 done.

We just have this one test:

    def test_cell_function(self):
        def f(neighbor, value):
            return value + 1 if neighbor else 0

        Cell.create_space(10, 10)
        cells = [Cell.at(x,0) for x in range(10)]
        cells.extend([Cell.at(4,y) for y in range(1,10)])
        room = Room(cells)
        result = dict()
        for cell, value in Cell.at(0,0).generate_with_function(func=f):
            result[cell.xy] = value
        assert result[(0,0)] == 0
        assert result[(4,0)] == 4
        assert result[(4,1)] == 5
        assert result[(5,0)] == 5
        assert result[(9,0)] == 9
        assert result[(4,9)] == 13

And here’s generate_with_function:

    def generate_with_function(self, func=None, condition=None, randomness=0.0):
        def accept_room(_cell):
            return _cell.is_in_a_room
        def nothing(cell, value):
            return 0
        f = func or nothing
        cond = condition or accept_room
        frontier = [(self, 0)]
        examined = {self}
        while frontier:
            next, value = frontier.pop(0)
            yield next, value
            examined.add(next)
            if random.random() < randomness:
                neighbors = next.random_neighbors
            else:
                neighbors = next.neighbors
            for neighbor in neighbors:
                if neighbor not in examined and cond(neighbor):
                    if neighbor not in frontier:
                        frontier.append((neighbor, f(next, value)))

We have no test for the nothing case. Let’s have one:

    def test_cell_function_default(self):
        Cell.create_space(10, 10)
        cells = [Cell.at(x,0) for x in range(10)]
        room = Room(cells)
        result = dict()
        for cell, value in Cell.at(0,0).generate_with_function():
            result[cell.xy] = value
        assert len(result) == 10
        assert all(value==0 for value in result.values())

That passes. Commit: add a test.

The initialization of frontier is wrong, defaulting to 0 rather than calling the function. My initial idea was that the provided function should have a special case where we’d pass in None as the Cell, but that makes no sense: we start with a Cell, not a None. We could pass None as the value but that would preclude ever using None as a valid return.

The bug is in the idea. The generate_with_function should take another parameter, initial_value and use that. It would be OK to default that to zero, I think.

Not so fast there, bunkie

Let’s think a bit, take a slightly wider view.

We currently have (at least) two generate methods, one that produces only the cells in flood order, and this new one that applies a function to each cell, including a sort of accumulating value and returning a tuple, the cell and its corresponding function value.

The two methods are terribly similar, with just a bit of difference in accommodating the function and the return of the tuple instead of just the cell. This duplication should be removed somehow. What options have we?

  1. We could only provide the new one and require everyone to accept a tuple. People wanting just the cell could just say:
        cell, _ = start.generate ...

  2. We could detect the lack of a provided function and produce just the cells rather than the tuples in that case.

  3. We could have a single generate into which we plugged various functions to create the frontier items and so on, configuring to get what we want.

  4. Maybe we should have a little object to support this capability, again perhaps with pluggable behavior.

Based on what I just typed and then erased, I realize that a single function that sometimes returns just cells and sometimes tuples is surely a bad idea. A function is much more usable if it always returns the same kind of thing. However, I really don’t like the idea of having two methods that look so much alike.

Is there some essential core to our two generate methods that both could use? We’ll explore that. First, though, let’s add in our init parameter and then see what we can see. We need another test:

    def test_function_init(self):
        def f(neighbor, value):
            return value + 1 if neighbor else 0

        Cell.create_space(10, 10)
        cells = [Cell.at(x,0) for x in range(10)]
        cells.extend([Cell.at(4,y) for y in range(1,10)])
        room = Room(cells)
        result = dict()
        for cell, value in Cell.at(0,0).generate_with_function(init=2, func=f):
            result[cell.xy] = value
        assert result[(0,0)] == 0 + 2
        assert result[(4,0)] == 4 + 2
        assert result[(4,1)] == 5 + 2
        assert result[(5,0)] == 5 + 2
        assert result[(9,0)] == 9 + 2
        assert result[(4,9)] == 13 + 2

Same layout, init to 2, result should all be two greater. Fails for want of init. This is all we need:

    def generate_with_function(self, init=0, func=None, condition=None, randomness=0.0):
        def accept_room(_cell):
            return _cell.is_in_a_room
        def nothing(cell, value):
            return 0
        f = func or nothing
        cond = condition or accept_room
        frontier = [(self, init)]

Test is green. Commit: new init parameter.

Rename func to function. Green. Commit.

I think we can gimmick this method so that if we give it a function it answers the tuple and if not the individual cells. If it were a private common method that might be OK. Oh, wait … we could certainly recode generate to call generate_with_function and then just yield the cell. Let me try that:

The obvious change did not work. 14 tests broke. And I found an issue that I do not understand: The generate_with_function starts like this:

    def generate_with_function(self, init=0, function=None, condition=None, randomness=0.0):
        def accept_room(_cell):
            return _cell.is_in_a_room
        def nothing(cell, value):
            return 0
        f = function or nothing
        cond = condition or accept_room

I used that default for cond because it was convenient. generate uses True. When I change this method to use True, tests loop forever. What’s up with that?

I change the tests to pass in the in a room condition, then change the default setting to True. Everything stays green, of course. Commit that.

Now a test without the condition, and we’ll see if we can figure out the issue. The test is the same as our first one, just no condition, and it loops.

Oh. It’s obvious. Look at this:

    def generate_with_function(self, init=0, function=None, condition=None, randomness=0.0):
        def accept_room(_cell):
            return True
        def nothing(cell, value):
            return 0
        f = function or nothing
        cond = condition or accept_room
        frontier = [(self, init)]
        examined = {self}
        while frontier:
            next_cell, value = frontier.pop(0)
            yield next_cell, value
            examined.add(next_cell)
            if random.random() < randomness:
                neighbors = next_cell.random_neighbors
            else:
                neighbors = next_cell.neighbors
            for neighbor in neighbors:
                if neighbor not in examined and cond(neighbor):
                    if neighbor not in frontier:
                        frontier.append((neighbor, f(next_cell, value)))

Those last two lines check for neighbor in frontier. That can never happen, since frontier is a tuple. So we always add something to frontier. The miracle is that it ever worked.

We can fix it readily I think:

...
for neighbor in neighbors:
    if neighbor not in examined and cond(neighbor):
        frontier_cells = (t[0] for t in frontier)
        if neighbor not in frontier_cells:
            frontier.append((neighbor, f(next_cell, value)))

Green. Commit: fix frontier defect.

I was going to stop now but let’s try again to have the old generate call the new one.

    def generate(self, condition=None, randomness=0.0):
        for cell, _ in self.generate_with_function(condition=condition, function=None, randomness=randomness):
            yield cell

As soon as I get the arguments all named, all the tests pass. This is just perfect. It is as it should be. Apropos this morning’s article Geek Joy, this is why I do this stuff. Very nice! Commit: generate uses generate_with_function.

Summary

I think we’re around 0.9 on generate_with_function and generate being what we want. I think the names need work, in particular with_function. And I’m sure we’d like to improve the code in the big function, and it seems to me to be a very good idea to convert both methods to require keyword arguments to avoid confusion about what is being asked for.

We are on the verge, the very verge of having our feature to find the cells furthest from a given cell. We just have the little Maximizing Machine to write, which will be fun and, I hope, rather nice.

A delicious outcome this morning. Just loved it when all the tests ran with that little tiny generate calling the new one. I hope you find something and someone to love today.

See you next time!