I have a bit of time and a bit of energy. Let’s scavenge Room to make a RoomFactory.

I think where we’re going will be to have a single RoomFactory with several entry methods, one for each kind of room. In each of those, it will accumulate cells for the room and then call a simple constructor in Room that accepts the cells and tucks them away. You could make a case for this already, since the room has lots of code for building and then, for now, just exists as a list of cells. Presumably in other parts of the game, whatever it is, the room would have other aspects.

Presently, other than being iterated to be drawn, the Room has nothing operational it it. That’s almost interesting on its own. Anyway my plan is to make a factory.

Let’s see who is using Room class now, to get a sense of the smoothest way to do it. There are 27 uses, including, for example:

main.py
    for _ in range(number_of_rooms):
        size = random.randint(90, 90)
        origin = bank.random_available_cell()
        room = Room(bank, size, origin)
        dungeon.add_room(room)


    def test_make_suite(self):
        space = CellSpace(10, 10)
        dungeon = Dungeon()
        o_0 = space.at(3,1)
        r_0 = Room(space, 1, o_0, '0')
        dungeon.add_room(r_0)

And they all look like that. For all of these, we’re building the original default “cavern” shape that grows by random accretion. So I’d like them all to change to something like:

room = RoomMaker().cave(space, size, origin)

I think probably we’ll move to passing the space in to the RomMaker constructor. Yeah, let’s do that.

Let’s start with a simple RoomMaker class that fakes it.

class RoomMaker:
    def __init__(self, space: CellSpace):
        self.space = space

    def cave(self, size: int, origin: Cell, name =''):
        return Room(self.space, size, origin, name)

Now, with any luck, I can just replace some of those Room creations with RoomMaker().cave.

Works. Perfect. Let’s just go ahead and fix them all, then we’ll evolve the code over. Find, some clever multi-cursor editing and we’re all green. Commit: all Rooms created with RoomMaker(space).cave().

Let’s see about moving the build over to the RoomMaker. I think it’ll be quick enough to copy over the methods and then change the Room constructor. Well, except we need to have a room to give the cells to. Unless … yes we’ll have Room adopt the cells when it gets them.

That goes easily and now we have:


class RoomMaker:
    def __init__(self, space: CellSpace):
        self.space = space
        self.cells:list[Cell] = []
        self.growth_candidates:list[Cell] = []

    def cave(self, number_of_cells: int, origin: Cell, name =''):
        self._build_cave(number_of_cells, origin)
        return Room(self.cells, origin, name)

    def _build_cave(self, number_of_cells, origin):
        new_cell: Cell = 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

    def _build_diamond(self, number_of_cells, origin):
        count = 0
        for cell in origin.generate(lambda c: c.is_available):
            self.take(cell)
            count += 1
            if count == number_of_cells:
                break

    def take(self, new_cell: Cell):
        self.cells.append(new_cell)
        new_cell.room = self

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


class Room:
    def __init__(self, cells, origin, name = ''):
        self.cells:list[Cell] = cells
        self.origin = origin
        self.name = name
        self.color = "grey22"
        for cell in self.cells:
            cell.room = self

    def __iter__(self):
        return iter(self.cells)

    def __repr__(self):
        if self.name != '':
            return f'Room({self.name})'
        return f'Room(unknown)'

It turns out that Rooms want to know their origin, though we might want to change that. Note that the Room just overrides the RoomMaker’s assignment of the room to itself.

Let’s sum up. There are Reuben sandwiches in my future.

Summary

That went smoothly, although there were 27 places needing to be changed. It was easy enough to do. Were we working at a larger scale, we would have had to rig up two ways to build a Room, one with provided cells and one where it did the job itself (probably by calling RoomMaker). I’m not sure exactly how I’d do that, probably with a messy but smarter __init__ in Room that determined whether it was being given a list of Cells or not, Wasn’t necessary here, but would have been easy enough if we needed some time to evolve to the Factory.

We are now well positioned to implement other schemes such as the diamond room, an actually roundish room if I can figure out how, and so on.

Once again, a seemingly substantial change turns out to be straightforward with a little care. Is everything like that? Probably not, but if we keep our objects close to being in good shape, things go pretty well.

After-Action Report

: Turns out that RoomMaker has memory and when one instance is used multiple times to make multiple rooms, bad things can happen. I have a fix that I do not like, basically re-initializing the class but that seems fraught. Will think on this.

"map of rooms looking good"