Better Borders
I have a bit of time this afternoon. Let’s see what we might do about borders that would be better.
I think we’ll rep;lace most of this code, but it’s working and mostly harmless, so I’ll commit and work from where we are with the hacked-in, er um official spike experimental version. Commit: border cell spike.
My approach now will be to figure out what an approximation to something one might actually want, given what we know now. In particular, I think that the BorderMap / BorderList thing might be useful and I’m pretty sure it has no other purpose now but to serve what we need. I’ll try not to damage it too badly, just in case it has some other utility.
Here’s that whole family:
@dataclass
class Border:
side: tuple
boundary: str
border_offsets: ClassVar[dict] = {
(-1, 0): ((0, 0), (0, 1)),
( 1, 0): ((1, 0), (1, 1)),
( 0, -1): ((0, 0), (1, 0)),
( 0, 1): ((0, 1), (1, 1)),
}
def select_boundary(self, a_dictionary):
return a_dictionary[self.boundary]
def line_coordinates(self, cell_xy, cell_size):
start_offset, finish_offset = self.border_offsets[self.side]
start = self.scale_to_size(start_offset, cell_xy, cell_size)
finish = self.scale_to_size(finish_offset, cell_xy, cell_size)
return start, finish
@staticmethod
def scale_to_size(offset, cell_coordinates, cell_size):
x_cell, y_cell = cell_coordinates
x_offset, y_offset = offset
x_scaled = x_cell * cell_size + x_offset * cell_size
y_scaled = y_cell * cell_size + y_offset * cell_size
return x_scaled, y_scaled
class BorderList:
def __init__(self, layout, cell):
self.borders = []
my_room = layout.get_room(cell)
for side in [(1,0), (-1,0), (0, -1), (0,1)]:
neighbor = layout.at_offset(cell.xy, side)
neighbor_room = layout.get_room(neighbor) if neighbor else None
border = my_room.border_type(neighbor_room)
self.borders.append(Border(side, border))
def __len__(self):
return len(self.borders)
def __iter__(self):
return iter(self.borders)
class BorderMap:
def __init__(self, layout):
self.borders = {}
for room in layout.rooms:
for cell in room:
borders = BorderList(layout, cell)
self.borders[cell] = borders
def __getitem__(self, cell):
return self.borders[cell]
We have one test for BorderMap and a couple for BorderList. Some for Border, checking the coordinate calculations. I don’t anticipate needing much of that but will try to retain it for now rather than break tests.
Wishful Thinking
I think what might be good would be to have a method, probably on BorderList, to help us out. There are, in principle, 16 possible borders, if we assume only open vs closed, one for each possible direction that might be closed. So what if we had a list of 16 items, and a method on BorderList that would return the appropriate item from the list?
Or, maybe BorderList just produces the number 0-15. Each Border, depending on direction, needs to return an integer, which will be zero when ‘open’ and 1, 2, 4, or 8 when not open.
def test_border_position_index(self):
border = Border((1,0), 'wall')
assert border.index() == 1
border = Border((0,1), 'wall')
assert border.index() == 2
border = Border((-1,0), 'wall')
assert border.index() == 4
border = Border((0,-1), 'wall')
assert border.index() == 8
I plan to sum the BorderList to get my 0-15 index.
class Border:
border_indexes: ClassVar[dict] = {
(1,0): 1,
(0,1): 2,
(-1,0): 4,
(0,-1): 8
}
def index(self):
if self.boundary == 'open':
return 0
else:
return self.border_indexes[self.side]
So far so good. Now let’s continue the test to get boundary state. Testing that will be difficult because BorderList creates itself during __init__ by scanning a real layout:
class BorderList:
def __init__(self, layout, cell):
self.borders = []
my_room = layout.get_room(cell)
for side in [(1,0), (-1,0), (0, -1), (0,1)]:
neighbor = layout.at_offset(cell.xy, side)
neighbor_room = layout.get_room(neighbor) if neighbor else None
border = my_room.border_type(neighbor_room)
self.borders.append(Border(side, border))
Let’s just make a TestBorderList that overrides the init.
class TestBorderList(BorderList):
def __init__(self, borders):
self.borders = borders
Extend the test (new name) to check the new method border_index:
def test_border_indexes(self):
borders = []
border = Border((1,0), 'wall')
borders.append(border)
assert border.index() == 1
border = Border((0,1), 'wall')
borders.append(border)
assert border.index() == 2
border = Border((-1,0), 'wall')
borders.append(border)
assert border.index() == 4
border = Border((0,-1), 'wall')
borders.append(border)
assert border.index() == 8
border_list = TestBorderList(borders)
assert border_list.border_index() == 15
Implement:
class BorderList:
def border_index(self):
return sum ([border.index() for border in self.borders])
And we’re green. Commit: BorderList can return border type index.
Now we need a bunch of tiles, in a dictionary I think, to look things up. This will not suffice but it should get us close.
I’ll modify the RoomView now:
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)
We see that this is not well-factored, because I had to duplicate the sizing. We’ll deal with that in due time.
And now, the tedious creation of the dictionary we need. I don’t have all the tiles I need and so I used some mud ones to give a pretty good picture of what is needed. Here’s the dictionary:
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'),
}
This needs plenty of cleanup, much better naming, and so on. But we do get this layout:

This was more work than I had planned, mostly trudging through the images for one that would work. I think I’d like better names, if only Wall_N and WALL_NW and WALL_S and WALL_WS and WALL_ENWS. Or perhaps I can come up with a computable name that we can use instead of our current 0-15. We’ll see.
I think if I were to continue those comments in the dictionary, in some orderly fashion, we might see how to name the tiles to meet our needs. Their names, after all, are there to serve us, not the other way around. And I don’t begrudge the tile maker his $10, but I do think better names were possible.
Still a bit messy, certainly. But we have walls appearing where we expect them. Nearly good! See you next time!