Yesterday’s work has been taken as an experiment in having different room shapes, via different room builders. today, Let’s get started on the real thing. I have some ideas. That’s never a good sign.

I had difficulty with an excessively clever notion of plugging in a builder method via some class creation methods. I think instead what we should do is build the room’s cell collection right in the class method and then pass it to the Room’s real constructor. In aid of that, I think we may need something a bit more clever in Cell and CellSpace. Currently, a Cell knows its space, so that it can find its neighbors, and it has a member variable room that is the Room instance that owns the Cell.

As things currently stand, when we build out a room, we select a random cell in the room, and then we check to see if it has any neighbors that are available, and if it has we select one to add to the room. This keeps the room contiguous but somewhat random. I’m looking at that code now, and I am suspicious:

class Room:
    def find_adjacent_cell(self):
        for cell in sample(self.growth_candidates, len(self.growth_candidates)):
            self.probe_count += 1
            neighbors = cell.neighbors
            for neighbor in sample(neighbors, len(neighbors)):
                if neighbor.is_available:
                    return neighbor
            self.growth_candidates.remove(cell)
        return None

This method seems to me to fondle the neighbor cells a bit more than would be ideal. It seems to me that the cell could be a bit more helpful here by returning available_neighbors. Let’s program by intention, or what I like to call “wishful thinking”. We write what we wish would work, and then we make it work. We have plenty of tests that should break until we get it right.

    def find_adjacent_cell(self):
        for cell in sample(self.growth_candidates, len(self.growth_candidates)):
            self.probe_count += 1
            neighbors = cell.available_neighbors
            for neighbor in sample(neighbors, len(neighbors)):
                if neighbor.is_available:
                    return neighbor
            self.growth_candidates.remove(cell)
        return None

There is no property available_neighbors, so 11 tests break. Easily remedied:

class Cell:
    @property
    def available_neighbors(self):
        return [neighbor
                for neighbor in self.neighbors
                if neighbor.is_available]

Now we should be able to change the sample code, and I think we could use random.choice. Ah, but there is an issue, what if the list is empty? I’ll have to be careful here.

Rename variable:

    def find_adjacent_cell(self):
        for cell in sample(self.growth_candidates, len(self.growth_candidates)):
            self.probe_count += 1
            available = cell.available_neighbors
            for neighbor in sample(available, len(available)):
                    return neighbor
            self.growth_candidates.remove(cell)
        return None

And then:

    def find_adjacent_cell(self):
        for cell in sample(self.growth_candidates, len(self.growth_candidates)):
            self.probe_count += 1
            available = cell.available_neighbors
            if available:
                return choice(available)
            else:
                self.growth_candidates.remove(cell)
        return None

All the tests are green except one that counts probes. Moving things around has futzed the random number generator, I expect. It was an approval test: I’ll just change its expectations.

Or maybe we should remove it and probe_count: it serves no useful purpose. It was there when I was checking to see if the growth_candidates idea was helping and it was.

Remove probe_count and the test. Green. Commit: Cell can return available_neighbors.

Why did you divert to do that?

I did that because I was thinking about how rooms know they are available and what room they are in, and thinking that we’re going to need to change that for today’s work. Here’s what goes on in Cell to determine is_available:

    @property
    def is_available(self) -> bool:
        return self.room is None

That seems somewhat OK, a Cell is available if it doesn’t have a Room. But as I think about new room builders in class methods, it seems to me that the class-level code will want to accumulate the cells that will go into the room and then pass them to the room constructor, which will plunk them away however it wants to. So the class method will want to “reserve” a cell. We’ll probably start by just stuffing any old value into room, perhaps Room class, but I suspect we might want something more robust, perhaps now, perhaps later. So … when I noticed the Room doing that inspection of the cells, I realized that we could defer that decision to the Cell. That’s inherently better, it didn’t take long, and it was consistent with what was in my mind.

So that’s why I did that.

Now let’s work toward a class method to do the current build procedure. Here’s Room as it stands:

class Room:
    def __init__(self, space: CellSpace, size, origin, name = ''):
        self.space = space
        self.origin = origin
        self.name = name
        self.cells:list[Cell] = []
        self.growth_candidates: list[Cell] = []
        # r = random.randint(0, 255)
        # g = random.randint(0, 255)
        # b = random.randint(0, 255)
        # self.color = pygame.Color(r, g, b)
        self.color = "grey22"
        self._build(size)

    def _build(self, number_of_cells):
        new_cell: Cell = self.origin
        for _ in range(number_of_cells):
            self.take(new_cell)
            self.growth_candidates.append(new_cell)
            new_cell = self.find_adjacent_cell()
            if new_cell is None:
                return

We’ve already seen find_adjacent_cell.

Ah. This code tells me something that I don’t much like but that is probably important. Even the simple _build we have here uses three methods of Room. We aren’t going to be able to do a good job of clever construction in a single method. We’re gonna need a RoomFactory concept.

Or else go back to the notion of a pluggable method. Factory is probably better.

Owing to other activities this morning, I think we’ll wrap up here.

Summary

I had high hopes for a bit more progress this morning, but I didn’t even get started until after 10 AM, because Hill (ptui!) wanted me to include the full article in my RSS feed, and that took me several hours yesterday, some study last night, and then a bunch of frustrating experiments this morning. It seems to be nearly working but it wasted most of my morning fuel.

As for the dungeon, we have a small improvement to Room and Cell, and a better sense of what we need to do. A RoomFactory will be the right thing, it’s just that I’ll probably have to change a bunch of tests. We’ll see. There are only 27 usages of Room, and they’ll probably all be similar. Or, we might do some grotesque hackery in Room.__init__.

One way or another, we’ll git ‘er done. See you next time!