We’ve agreed that our current borders scheme works, but isn’t quite what we need. Let’s see how to make some progress in how we make this dungeon. This pass, we prepare the soil.

I have a bit of a theory about what we might do. Let me lay it out and then we’ll look at the code we have and decide what we can do.

I’m tentatively proposing a small set of standard floor tile asset names that embody some semantics that both humans and computers can deal with. There are 16 combinations of borders, counting cells with no borders and cells with borders on all four sides. We could give the combinations somewhat meaningful names:

Name Purpose
F Regular flooring
E Tile with border on east
N Tile with border on north
EN Borders on east and north
ENW Borders east, north, west
ENWS Borders east, north, west, south

Then we could build a dictionary with those keys, pointing to a list of asset file names that could serve in that position. Probably we would have an associated list of weights, which we know we need for the Floor if not for other positions. (I think it likely that we’ll only have one tile for each border situation but more would be nice and we should allow for it.)

Because there are already two types of flooring, tile and mud, and perhaps more to come, at least in a real program, we should expect to have more than one such table, one for mud rooms, one for tile rooms, and so on. Rooms tile themselves, so we could have the room pick its asset table.

I like the ENWS notation better than the 1 2 4 8 we have now.

Let’s review what we have now. I’m sure we have code to remove as well as code to improve.

class RoomView:
    boundary_colors = {"open":arcade.color.BLACK,
                       "partition":arcade.color.GREEN,
                       "door":arcade.color.WHITE,
                       "wall":arcade.color.RED}
    path = '/Users/ron/PycharmProjects/dungeon/assets/images/'
    texture_dictionary = {
        0: arcade.load_texture(path+'Tile (21).png'), # none
        1: arcade.load_texture(path+'Floor_Edge_29.png'), # east
        2: arcade.load_texture(path+'Floor_Edge_36.png'), # north
        3: arcade.load_texture(path+'Wall_81.png'),
        4: arcade.load_texture(path+'Floor_Edge_33.png'), # west
        5: arcade.load_texture(path+'Wall_67.png'),
        6: arcade.load_texture(path+'Wall_80.png'),
        7: arcade.load_texture(path+'Wall_66.png'),
        8: arcade.load_texture(path+'Floor_Edge_25.png'), # south
        9: arcade.load_texture(path+'Wall_55.png'),
        10: arcade.load_texture(path+'Wall_3.png'),
        11: arcade.load_texture(path+'Wall_4.png'),
        12: arcade.load_texture(path+'Wall_53.png'),
        13: arcade.load_texture(path+'Wall_69.png'),
        14: arcade.load_texture(path+'Wall_1.png'),
        15: arcade.load_texture(path+'Wall_57.png'),
    }

    def __init__(self, dungeon, room):
        self.dungeon = dungeon
        self.layout = dungeon.layout
        self.room = room
        path = '/Users/ron/PycharmProjects/dungeon/assets/images/'
        f21 = arcade.load_texture(path + 'Tile (21).png')
        fgrate = arcade.load_texture(path + 'Floor_13_Grateb.png')
        f22 = arcade.load_texture(path + 'Tile (22).png')
        self.fnorth = arcade.load_texture(path + 'Tile (36).png')
        self.textures = [f21, f22, fgrate]
        self.weights = [20, 5, 1]

    def create(self, shape_list):
        cells = set(self.room.cells)
        for cell in cells:
            self.choose_flooring(cell, shape_list)

    def choose_flooring(self, cell, shape_list):
        borders: BorderList = self.layout.get_borders(cell)
        border_type = borders.border_index()
        if border_type == 0:
            sprite = self.choose_normal_flooring(cell)
        else:
            texture = self.texture_dictionary[border_type]
            sprite = arcade.Sprite(texture, scale=1 / 16)
            dx = cell.x * cell_size + cell_size // 2
            dy = cell.y * cell_size + cell_size // 2
            sprite.position = (dx, dy)
        shape_list.append(sprite)

    def choose_normal_flooring(self, cell):
        texture = random.choices(self.textures, self.weights)[0]
        sprite = arcade.Sprite(texture, scale=1 / 16)
        dx = cell.x * cell_size + cell_size // 2
        dy = cell.y * cell_size + cell_size // 2
        sprite.position = (dx, dy)
        return sprite

To tell the truth, this isn’t clean enough to begin to work on anything more grand. Let’s see about improving what we have until it is closer to what we might want.

First thing I notice is that massive table, and that’s the thing we’re wanting to improve. But I also note the duplication of scaling of the sprite, and I think we should consolidate that. Let’s extract from choose_normal_flooring. PyCharm actually identifies the duplicated lines. It offers no solution, but we extract:

    def choose_flooring(self, cell, shape_list):
        borders: BorderList = self.layout.get_borders(cell)
        border_type = borders.border_index()
        if border_type == 0:
            sprite = self.choose_normal_flooring(cell)
        else:
            texture = self.texture_dictionary[border_type]
            sprite = self.create_sprite(texture, cell)
        shape_list.append(sprite)

    def choose_normal_flooring(self, cell):
        texture = random.choices(self.textures, self.weights)[0]
        sprite = self.create_sprite(texture, cell)
        return sprite

    def create_sprite(self, texture, cell):
        sprite = arcade.Sprite(texture, scale=1 / 16)
        dx = cell.x * cell_size + cell_size // 2
        dy = cell.y * cell_size + cell_size // 2
        sprite.position = (dx, dy)
        return sprite

PyCharm did find and replace the duplicated code. I don’t love this as much as I might. Revert that.

Instead, we’ll make it worse, by inlining choose_normal_flooring.

    def choose_flooring(self, cell, shape_list):
        borders: BorderList = self.layout.get_borders(cell)
        border_type = borders.border_index()
        if border_type == 0:
            texture1 = random.choices(self.textures, self.weights)[0]
            sprite1 = arcade.Sprite(texture1, scale=1 / 16)
            dx1 = cell.x * cell_size + cell_size // 2
            dy1 = cell.y * cell_size + cell_size // 2
            sprite1.position = (dx1, dy1)
            sprite = sprite1
        else:
            texture = self.texture_dictionary[border_type]
            sprite = arcade.Sprite(texture, scale=1 / 16)
            dx = cell.x * cell_size + cell_size // 2
            dy = cell.y * cell_size + cell_size // 2
            sprite.position = (dx, dy)
        shape_list.append(sprite)

PyCharm made an odd decision with that sprite1 name. We can do without that and the weird texture1 and similar names.

    def choose_flooring(self, cell, shape_list):
        borders: BorderList = self.layout.get_borders(cell)
        border_type = borders.border_index()
        if border_type == 0:
            texture = random.choices(self.textures, self.weights)[0]
            sprite = arcade.Sprite(texture, scale=1 / 16)
            dx1 = cell.x * cell_size + cell_size // 2
            dy1 = cell.y * cell_size + cell_size // 2
            sprite.position = (dx1, dy1)
        else:
            texture = self.texture_dictionary[border_type]
            sprite = arcade.Sprite(texture, scale=1 / 16)
            dx = cell.x * cell_size + cell_size // 2
            dy = cell.y * cell_size + cell_size // 2
            sprite.position = (dx, dy)
        shape_list.append(sprite)

I missed the dx1 and dy1, but the next move will fix it. I could move the sprite creation and scaling down manually, but let’s try the extract again now. This time it doesn’t recognize the duplication, so I revert it and do this manually:

    def choose_flooring(self, cell, shape_list):
        borders: BorderList = self.layout.get_borders(cell)
        border_type = borders.border_index()
        if border_type == 0:
            texture = random.choices(self.textures, self.weights)[0]
        else:
            texture = self.texture_dictionary[border_type]
        sprite = arcade.Sprite(texture, scale=1 / 16)
        dx = cell.x * cell_size + cell_size // 2
        dy = cell.y * cell_size + cell_size // 2
        sprite.position = (dx, dy)
        shape_list.append(sprite)

That method clearly does two things, choosing a texture and then making a sprite. Split it.

    def choose_flooring(self, cell, shape_list):
        texture = self.choose_flooring_texture(cell)
        sprite = arcade.Sprite(texture, scale=1 / 16)
        dx = cell.x * cell_size + cell_size // 2
        dy = cell.y * cell_size + cell_size // 2
        sprite.position = (dx, dy)
        shape_list.append(sprite)

    def choose_flooring_texture(self, cell):
        borders: BorderList = self.layout.get_borders(cell)
        border_type = borders.border_index()
        if border_type == 0:
            texture = random.choices(self.textures, self.weights)[0]
        else:
            texture = self.texture_dictionary[border_type]
        return texture

Extract again from choose_flooring:

    def choose_flooring(self, cell, shape_list):
        texture = self.choose_flooring_texture(cell)
        sprite = self.make_adjusted_sprite(cell, texture)
        shape_list.append(sprite)

    def make_adjusted_sprite(self, cell, texture):
        sprite = arcade.Sprite(texture, scale=1 / 16)
        dx = cell.x * cell_size + cell_size // 2
        dy = cell.y * cell_size + cell_size // 2
        sprite.position = (dx, dy)
        return sprite

Feature Envy there on the dx dy stuff. Cell should help here.

class Cell:
    def center_position(self, size):
        cx = self.x*size + size // 2
        cy = self.y*size + size // 2
        return cx, cy

class RoomView:
    def make_adjusted_sprite(self, cell, texture):
        sprite = arcade.Sprite(texture, scale=1 / 16)
        sprite.position = cell.center_position(cell_size)
        return sprite

Green and the picture is good. Commit: refactoring RoomView.

One more tiny improvement, from this:

    def choose_flooring_texture(self, cell):
        borders: BorderList = self.layout.get_borders(cell)
        border_type = borders.border_index()
        if border_type == 0:
            texture = random.choices(self.textures, self.weights)[0]
        else:
            texture = self.texture_dictionary[border_type]
        return texture

To this:

    def choose_flooring_texture(self, cell):
        borders: BorderList = self.layout.get_borders(cell)
        border_type = borders.border_index()
        if border_type == 0:
            return random.choices(self.textures, self.weights)[0]
        else:
            return self.texture_dictionary[border_type]

Commit: tidying. Let’s pause and reflect, and probably stop because you have better things to do than read even more of this.

Reflection

I can’t say that I planned this outcome, but our state at this point is interesting. We still have our table, which is indexed by 0-15, which we really don’t like, because it’s hard to work out what texture belongs with 13. (13 = 1 + 4 + 8 = east, west, south, upward-facing cup.)

Here, in the method above, is the only code that cares what comes back from border_index. We can decide new keys, change what BorderList returns, change the dictionary, and things are better. Now it’s certainly true that we could have done that first: we haven’t really changed that flow at all. But our code now makes it far more clear what’s going on, and, at least for me, gives a sense of being on the right track.

And, along the way, we have a much more reasonable bit of code for doing flooring including borders.

We see, looking at choose_flooring_texture that we have an asymmetry, allowing the main flooring tiles to be selected from a weighted list, but the border ones are all expected to have just a single choice. And we see exactly what we’d have to do to make it symmetric: return a list of cells and weights for each position in the texture mapping dictionary.

I don’t know anything at all about gardening, but I suspect you don’t just walk out in the yard and stab carrot seeds down into the ground. I suspect you need to get rid of the grass, turn over the soil a bit, and so on, to prepare the garden to grow carrots. I do know a bit more about programming than I do about gardening, and I find that before I do a thing, it generally pays off to prepare the ground a bit before setting out to make changes.

I think it has paid off here. And I think we’ll plant the carrot seeds next time. For now, I have chai to drink and books to read.

See you soon!