OODA
Let’s look around, get aligned, decide what to do, then do it. Well, we do a little, but not much. Have I learned anything? PSA: Eat the rich!
- Added in Post
- We get a bit of improvement, but I freely grant that I suspect the past few days, while interesting at least to me, have not been terribly productive in terms of new capability or vastly better code. In terms of having better design ideas, I think so, but they’re not improving the code much as yet.
Intuitively, I think we’re in a somewhat better situation with the CellBank serving as a permanent container for all the Cells instead of passing them around between the bank and other holders.
- Idea!
- As I was thinking how to start the next sentence, I began to think about the fact that Cells have a pointer back to their
space, the CellBank. Pointers like that are considered to be a bit iffy, because maintaining two-way pointers is harder than one way. My mind flashed back to the experiment a few days back where I tried to build a mesh of Cells and declared it to be too hard. -
It comes to me that since all the cells are created when we create the CellBank, and since they know how to find their neighbors using the CellBank, it would be easy and safe to have them cache their neighbor cells, resulting in the mesh from that plan. That would certainly be more efficient than recalculating them all the time.
-
Maybe the mesh idea will come back. That would be amusing. But I digress …
As I was saying before I so rudely interrupted myself, the current scheme has pointers in the containers to the cells, and the cells have pointers back to their primary container, and, in my present thinking, to the other objects that might contain them, such as a room or a path. It is entirely possible that this back pointer isn’t needed, but we do need some kind of indication of whether a cell is in open space or inside a structure such as a room. Maybe it just needs a status bit or something. That would be simpler, unless we have a need to know specifically where the cell resides.
- Note
- I think I’ve been doing too much speculation for a few days, and that I’ve put too much speculation into the code. As brother GeePaw Hill puts it, we are in the business of changing code, and there is no huge need to design for the deep future, although one could certainly do too little design, which is its own kind of trouble.
-
But perhaps I’d have done better, and saved literally four or five hours had I decided to refactor in the capabilities I wanted?
-
I think not. Within reason, thinking is good. It’s important to ground that thinking with code, and in fact we’ve done that for the ideas that came up. So we tested the extent to which the ideas would fly. As we understood them then, they flew in the same sense that a brick does.
-
We learned. Learning is arguably the main thing we need to do as we develop a system.1
There I go again. Let’s look back at the original concerns that led me to look for a better scheme. It started earlier but the Design Thinking article brought some issues to the fore:
- Searches: It seemed that there were lots of loops doing searches for things.
- Periphery: Room creation inefficiently checks many cells that can’t extend the room.
- Fascinating!
- Those are, I think, the only real issues mentioned in that article. Then I went spinning off into the past few days of experimentation. Not much focus on need, lots of speculation.
-
In my defense, I’m doing this for fun—certainly no one is paying me to do it—and so it’s OK with me wherever it all leads. But in terms of product development productivity, I might have been more focused. Let’s see if we can improve focus now.
Issues
OK let’s talk about real concerns.
- I think the main program will not run at all: I think it may not be converted to be consistent with the current modified scheme.
- Building the dungeon takes a few seconds, and that feels too slow to me. Issues include slow pathfinding and the fact that a Room doesn’t know its peripheral cells.
- No real point: I just started doing this because Hill was doing something similar and I wanted to try it. Should I try to make this into something?
- Objects “not helping”. This is a subjective feeling that things aren’t quite right.
Decide
First, we’ll make sure that main works, so that we can see how we’re doing.
Then, let’s focus on improving dungeon build time, addressing both room creation and pathfinding. We’ll pay closer attention to the objects helping us and see whether we can improve performance and design as well. After that, we’ll see whether this program moves to some new phase of wonderfulness.
Main
This should come down to running it to see why it explodes.
I find a cell creation that doesn’t include the bank:
def main():
bank = CellBank(64, 56)
# random.seed(234)
dungeon = Dungeon()
number_of_rooms = random.randint(12, 12)
for _ in range(number_of_rooms):
size = random.randint(90, 90)
y, y = random_available_cell(bank)
origin = Cell(x, y, None)
room = Room(bank, size, origin)
dungeon.add_room(room)
for room_1, room_2 in zip(dungeon.rooms, dungeon.rooms[1:]):
dungeon.find_path_between_rooms(bank, room_1, room_2)
view = DungeonView(dungeon)
view.main_loop()
I think we can do this:
...
size = random.randint(90, 90)
origin = random_available_cell(bank)
room = Room(bank, size, origin)
dungeon.add_room(room)
...
Yes, that gets us to draw_border in RoomView:
class RoomView:
def draw_border(self, cell, cells, screen):
x, y = cell
cx0 = cell.x*cell_size
cy0 = cell.y*cell_size
cx1 = cx0 + cell_size
cy1 = cy0 + cell_size
neighbor_count = 0
for neighbor in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
dx, dy = neighbor
check = Cell(x + dx, y + dy, None)
if check not in cells:
neighbor_count += 1
if neighbor == (-1,0):
#left
self.line(cx0, cy0, cx0, cy1, screen)
elif neighbor == (0,1):
#bottom
self.line(cx0, cy1, cx1, cy1, screen)
elif neighbor == (0,-1):
#top
self.line(cx0, cy0, cx1, cy0, screen)
elif neighbor == (1,0):
#right
self.line(cx1, cy0, cx1, cy1, screen)
This code is definitely a case of objects not helping. We are looking for cells in the room that have neighbors that are not in the room, and then we use the coordinates at which there is no in-room neighbor to draw a border in that direction.
I would like to make it work, and then make it right, although if we can move toward right while making it work that would be ideal. It’s worth noting that this code runs on every draw cycle, and that it searches every cell in the room and draws, most commonly, no border. The notion of periphery would be helpful here as well as in room creation.
The RoomView only has the Room instance at present. It really needs access to the space (CellBank, remind me to rename that) to get to the cells it needs. The Room does not currently have the space: it uses it but throws it away:
class Room:
def __init__(self, bank, size, origin):
self.origin = None
self.cells:list[Cell] = []
r = random.randint(0, 255)
g = random.randint(0, 255)
b = random.randint(0, 255)
self.color = pygame.Color(r, g, b)
# print(self.color)
self._build(bank, size, origin)
Let’s quickly fix this issue, then see about making it right. Cache the bank (rename to space while we’re here) and then the RoomView can use it, or, perhaps better, the room, to get what it needs.
class Room:
def __init__(self, space, size, origin):
self.space = space
self.origin = None
self.cells:list[Cell] = []
r = random.randint(0, 255)
g = random.randint(0, 255)
b = random.randint(0, 255)
self.color = pygame.Color(r, g, b)
# print(self.color)
self._build(space, size, origin)
As we see in the last line there, we are passing the space around. Now that it’s a member, we should use it. But we are on a quest to make main work just now.
- Silent Scream
- This gets worse and worse. This new scheme really wants the space to be available to almost everyone. Anyway we can use the space now, although it is a Demeter violation. Or we can send a message to the room asking for the cell at a given set of coordinates. Let’s do that.
class RoomView:
for neighbor in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
dx, dy = neighbor
n_x = x + dx
n_y = y + dy
check = self.room.cell_at(n_x, n_y)
And …
class Room:
def cell_at(self, x, y):
return self.space.at(x, y)
That might do the trick. Run and see what breaks now. It’s alive!!

We’re green. Commit: main fixed to work with new space mods.
We’d do well to think about what we’ve just done, to see whether we need to do better.
Our fist change was to use the CellBank to get the cell to prime a new room. That is as it should be. However, there’s something not right about this:
def main():
bank = CellBank(64, 56)
# random.seed(234)
dungeon = Dungeon()
number_of_rooms = random.randint(12, 12)
for _ in range(number_of_rooms):
size = random.randint(90, 90)
origin = random_available_cell(bank)
room = Room(bank, size, origin)
dungeon.add_room(room)
for room_1, room_2 in zip(dungeon.rooms, dungeon.rooms[1:]):
dungeon.find_path_between_rooms(bank, room_1, room_2)
view = DungeonView(dungeon)
view.main_loop()
We create a Dungeon and then add rooms to it from the outside. We could certainly imagine a creation method on the Dungeon class, which might be better. I do think there are a few different algorithms for creating rooms: I’ve tried fully random and centering them around fixed points, and I have another scheme in mind, so possibly we’re OK treating Dungeon as a container for rooms. It is odd that Rooms know the bank/space and the Dungeon does not.
Dungeon is pretty weak. It has a method find_path_between_rooms, but all that does is create a PathFinder and delegate to it. It doesn’t even know the territory (space). I’ve made a note of that. Maybe it’s OK as it stands, but maybe the room creation should be mediated through Dungeon somehow. Or maybe we need some DungeonFactory objects.
Our second change actually improved things slightly, I think. But the method is a bit nasty, even after a bit of tidying:
def draw_border(self, cell, cells, screen):
x, y = cell
cx0 = cell.x*cell_size
cy0 = cell.y*cell_size
cx1 = cx0 + cell_size
cy1 = cy0 + cell_size
neighbor_count = 0
for neighbor in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
dx, dy = neighbor
n_x = x + dx
n_y = y + dy
check = self.room.cell_at(n_x, n_y)
if check not in cells:
neighbor_count += 1
if neighbor == (0,1):
#bottom
self.line(cx0, cy1, cx1, cy1, screen)
elif neighbor == (0,-1):
#top
self.line(cx0, cy0, cx1, cy0, screen)
elif neighbor == (-1,0):
#left
self.line(cx0, cy0, cx0, cy1, screen)
elif neighbor == (1,0):
#right
self.line(cx1, cy0, cx1, cy1, screen)
There’s got to be a pattern in there. Or maybe just use a table. Let’s think.
I notice that the pattern starts with three occurrences of cx0, which is correct but odd. The lines are drawn like this:
01 ----> 11
^ ^
| |
| |
00 ----> 10
What if we arranged them in a cycle, would a pattern appear?
01 ----> 11
^ |
| |
| v
00 <---- 10
No help. How about this:
| x= | cx_ | cx_ |
|---|---|---|
| -1 | 0 | 0 |
| 0 | 0 | 1 |
| +1 | 1 | 1 |
| y= | cy_ | cy_ |
|---|---|---|
| -1 | 0 | 0 |
| 0 | 0 | 1 |
| +1 | 1 | 1 |
I think that’s correct. The X value determines the x values where we start and finish, and the y value determines start and ending y. Is it useful? I think if we were to use it, it would become completely impossible to figure out what was going on.
I think we’ll belay this concern for now. There might be something better but this is at least straightforward if not elegant.
Reflection
Not gonna lie, the past few days have been entertaining for me, and there are some ideas lurking that may yet be useful, but there hasn’t been any really substantial improvement to the code as yet. I do prefer having a permanent space that always holds all the cells, rather than passing then around between the bank and rooms. I think that will keep things a bit simpler, although there is still evolution needed to reap the benefits. The benefits will not be large, I suspect, but they’ll be real. Worth the doing? We’ll never know, we can’t read the results from the time line where we didn’t do it.
Hmm. I wonder if a CellView object would be helpful. As things stand, the RoomView is sorting out how to draw the cells. Maybe a dedicated CellView class would be good to have.
Relatedly, we create RoomView objects on the fly, even though nothing changes in the picture after we first draw it. Certainly the rooms don’t change. Generally, however, I don’t worry about creating and destroying little objects. That’s what garbage collectors are all about. Still, it gives one to wonder.
I think that next time we’d better bear down and work on the periphery notion, or, failing that, on improving pathfinding. I have in mind a very different approach to pathfinding, so we’ll probably start with improving room creation.
Hm. Fun but not productive. Still, no one is paying for this. Fun is OK, and thinking about code is always fun for me.
If you’re still with me, maybe it’s fun for you. Maybe after LLMs take over (never happen)2 we’ll do code as an art form. That would be fun. Remind me to write about that.
See you soon!
-
Arguably the strongest personal argument against using “AI” (ptui!) in programming is that it will reduce the programmer’s understanding of the program, and that is bad.3 ↩
-
Well, I hope they never take over. They’re not very good, but the oligarchy owns them, prefers them, and would gladly not pay any of us to do any job if they can get it done nearly as well with software or robots. We really need to start eating the rich. ↩
-
Here are some better arguments pointing out that they are bad for humans and the world. ↩