Borders?
I think I’m ready to draw smarter borders. I have chunks of a plan. Green shoes.
I drew another picture last night:

There are a lot of p-baked1 ideas in there, including:
- Might be useful to have directions
e,n,w,sas convenience methods for accessing a particular neighbor or fact about a cell. - Might be useful to have an actual Neighbor class, but why would that not just be another Cell?
- Might want methods like
neighbors,neighbors_randomized,available_neighbors,adjacent_neighbors,border(returning a list or dictionary). - A couple of sketched dictionary comprehensions that might return useful structures for use in a view.
- A simple method on Cell taking a room and a direction and answering what kind of border / door is needed, including a plain border, a blank border, or a door border.
Much of what’s there is just notes made while thinking. The list of possible methods is a good example of the kind of thinking I do about objects, just thinking about things that an object might provide, could provide. That gives me a sense of what the code might look like. I never fully trust ideas like that to be right, but quite often they are clearly interesting2 but seem not very useful. The thinking just kind of fills the idea bucket, which may come in handy later.
How can we put all this to use?
The best way that I know to find out how to put ideas to use is to try to use them. In the case of these ideas, we’re trying to come up smarter borders. Let’s try to put some meat on those bones. We scan each cell in each room, deciding what borders to put on it. The border depends on what is adjacent to that cell in the direction of the border. Let’s set out to do this:
When the adjacent cell is:
- in the same room, draw no border;
- in a different room, draw a green border;
- in the unused space, draw a red border;
We’ll have a method like the one in my sketch, taking a room and a direction, and answering with some simple thing, maybe a string or enum. We’ll might consider accepting a function parameter and calling it, just to see if that’s useful. We’ll decide that based on … well, based on whether when we get there I feel it might be worth trying.
One thing that we’ll surely need to do to make this work is change things so that when a room takes an available cell, it sets the cell’s owner to the room itself. We’ll test that in TestRoom:
def test_room_sets_owner(self):
space = CellBank(64, 56)
start = space.at(32, 32)
room = Room(space, 1, start)
cell = space.at(32, 32)
assert cell.owner == room
This duly fails. Let’s see what the room does:
def _build(self, bank, length, start_cell: Cell):
self.origin = start_cell
new_cell = start_cell
for _ in range(length):
bank.take(new_cell)
self.cells.append(new_cell)
self.periphery.append(new_cell)
new_cell = self.find_adjacent_cell(bank)
if new_cell is None:
return
We take the cell from the space (bank), but we don’t tell the space who is taking it. Let’s change the signature on the space, from this:
class CellBank:
def take(self, cell: Cell):
self.take_xy(cell.x, cell.y)
To this:
def take(self, cell: Cell, owner):
self.take_xy(cell.x, cell.y)
I specify that callers pass None as the default, which keeps things running no worse than they were. We need to change the signature on take_xy:
def take_xy(self, x, y):
cell = self.cell_array[x][y]
cell.take()
I feel like I could just do all this but in fact if I would belay my new test I could be committing all these changes, since they’re not breaking anything, so the small steps are useful in that when I do break something, it’ll become obvious just what is wrong.
def take_xy(self, x, y, owner):
cell = self.cell_array[x][y]
cell.take()
def take(self, cell: Cell, owner):
self.take_xy(cell.x, cell.y, owner)
PyCharm made a mistake! It refactored
bank.take_xy(*t)
to
bank.take_xy(,
I’ll try to remember to send them a bug report. Easily fixed this time.
Change signature of Cell.take:
def take(self, owner):
self.owner = owner
Change take_xy to use owner:
def take_xy(self, x, y, owner):
cell = self.cell_array[x][y]
cell.take(owner)
Additional tests are breaking! Now I wish I had been committing on each step. I’ll take a quick look and if it isn’t easy to fix, we’ll reset.
The fix was to complete the job:
def _build(self, bank, length, start_cell: Cell):
self.origin = start_cell
new_cell = start_cell
for _ in range(length):
bank.take(new_cell, self) # <===
self.cells.append(new_cell)
self.periphery.append(new_cell)
new_cell = self.find_adjacent_cell(bank)
if new_cell is None:
return
We are green. Commit: taking a cell sets owner to provided value.
Now we are ready to write our border characterization test.
I’m not sure I like this, but it should draw out the behavior:
def test_border_characterization(self):
space = CellBank(5, 5)
space.take_xy(3,3, 1)
space.take_xy(2,3, 1)
space.take_xy(4,3, 2)
c_33 = space.at(3,3)
borders = c_33.borders()
assert len(borders) == 4
assert borders["n"] == "wall"
assert borders["e"] == "door"
assert borders["w"] == "open"
assert borders["s"] == "wall"
To no one’s surprise, this test fails. Let’s code:
class Cell:
def borders(self):
result = dict()
for k,v in {'n':(0,-1), 's':(0,1), 'e':(1,0), 'w':(-1,0)}.items():
result[k] = self.border_at(*v)
return result
def border_at(self, x, y):
owner = self.owner
neighbor = self.at_offset(x, y)
if neighbor.owner == self.space:
return "wall"
elif neighbor.owner == owner:
return "open"
else:
return "door"
The test passes. Let’s commit, it can do no harm: cell can return border characterization.
Now I want to try this in RoomView, not least because I want to see if I have north and south sorted correctly. (I suspect that I have them upside down, owing to confusion over which way is North in PyGame.)
class RoomView:
def draw_border(self, cell, my_cells, screen):
x, y = cell
lines = self.make_lines(cell)
neighbor_count = 0
for neighbor_offset in lines.keys():
check = cell.at_offset(*neighbor_offset)
if check not in my_cells:
neighbor_count += 1
self.line(*lines[neighbor_offset],screen)
def make_lines(self, cell) -> dict[tuple[int, int], tuple[int | Any, int | Any, int | Any, int | Any]]:
cx0 = cell.x * cell_size
cy0 = cell.y * cell_size
cx1 = cx0 + cell_size
cy1 = cy0 + cell_size
bottom = (cx0, cy1, cx1, cy1)
top = (cx0, cy0, cx1, cy0)
left = (cx0, cy0, cx0, cy1)
right = (cx1, cy0, cx1, cy1)
lines = {(0, 1): bottom, (0, -1): top, (-1, 0): left, (1, 0): right}
return lines
def line(self, cx0, cy0, cx1, cy1, screen):
pygame.draw.line(screen, "white", (cx0, cy0), (cx1, cy1))
I might be a bit sorry that I refactored this yesterday, it might have been easier to change if left open. Remind me to discuss that.
OK what do I wish I could say here? Too hard. Let’s hack color in and then see what it looks like:
def draw_border(self, cell, my_cells, screen):
borders = cell.borders()
colors = {"open":"black", "door":"green", "wall":"red"}
directions = {'n':(0,-1), 's':(0,1), 'e':(1,0), 'w':(-1,0)}
lines = self.make_lines(cell)
for direction, neighbor_offset in directions.items():
color = colors[borders[direction]]
self.line(*lines[neighbor_offset], color, screen)
def make_lines(self, cell) -> dict[tuple[int, int], tuple[int | Any, int | Any, int | Any, int | Any]]:
cx0 = cell.x * cell_size
cy0 = cell.y * cell_size
cx1 = cx0 + cell_size
cy1 = cy0 + cell_size
bottom = (cx0, cy1, cx1, cy1)
top = (cx0, cy0, cx1, cy0)
left = (cx0, cy0, cx0, cy1)
right = (cx1, cy0, cx1, cy1)
lines = {(0, 1): bottom, (0, -1): top, (-1, 0): left, (1, 0): right}
return lines
Somewhat messy, because the View wants to use the offsets and the borders return n, s, e, w. We can change that, we’re here to please the View. The map looks just as I intended:

That code needs cleanup but it also works as intended. Commit: RoomView draws colored borders as intended.
I am tired. This is not the time to improve this code. I’m OK with committing it, because it works as intended, but I am not OK with calling the story done until I get a refactoring pass made. Probably next time.
Summary
The process of changing signatures went fairly well, with the disappointing step where PyCharm seems to have made a mistake. (It is possible that I fumble-fingered the keyboard and broke that code: I’ll have to try to generate a bug report.)
The decision to allow None to pass through to room ownership is questionable. And perhaps you noticed that in my test I didn’t pass a room at all, I passed integers 1 and 2. We aren’t sending messages to the values passed in, so that was sufficient to get the test to pass. Since Rooms initialize on creation, which I like, creating test rooms that are adjacent is a bit tricky, so I finessed that issue. In a more strict language, we’d have had to go another way.
I think I should have done more commits. There was a moment when I feared I’d have to roll back and there wasn’t a safe spot just a step away. Fortunately the bug was that obviously broken statement.
I promised to discuss whether I’m sorry I refactored the border drawing yesterday. I am not, and even if I was, I would not hesitate to do it again. Why? Because having better code pays off almost always, if not always. If I were as good as I’d like to be, I’d never pass up a chance to improve the code.
I have a feeling that is growing. I feel that what I’d like to be able to do is to send a message to a cell, passing it a few methods, one of which would be called back depending on the neighbor situation. I’ll let that idea sit: it’s certainly possible. I’m just not sure that I like it.
So. Good result, and the RoomView border code needs improvement, which may cause us to change our wishes about how the borders method on Cell works, returning a different dictionary or something. We’ll see.
For now, I’m pleased. See you next time!
- P.S.
- An idea popped into my head as I was fixing a typo: we now draw all four sides of each room’s border. We can possibly rig up a simpler drawing scheme that just injects the colors and treats the lines as fixed. This may or may not have actual meaning.