Not Best Pleased
Hello, loves! Tout le monde déteste l’IA.
I had in mind to do some pleasant Making App additions to the program. I’m afraid that the code may not agree. Result: whining is never really wasted.
- Added in Post
- I thought I was in big trouble in what follows. Turns out I was not. A small discovery is the key to a much better resolution than I had expected. Life is good!
Next on my agenda for the program is to improve the Making App side of things, so that it’s easier to assess how the allocation of things in the dungeon is going. I lot of where things go needs to make sense visually, so I want the ability to “pause” the dungeon generation at various stages, to see how things are going. I can even imagine that we’d add some control so that we could pause, look, then choose some particular addition or operation to do next. Essentially tuning how our random dungeon works.
I had in mind that that would be a nice break, after the long series of refactoring articles.
Yesterday, I did a quick spike that zoomed out the display so the whole dungeon is visible. That was easy, and I think it’ll be fairly easy to do it better if we need to. So far so good.
Then I tried stopping the generation “in the middle”. And there we have a problem.
Dungeon generation begins with the main program, or any function that wants a dungeon, generating rooms into the space of all possible cells. A Room is just an object in the Layout, containing some contiguous cells. It can have any shape that the program knows how to generate. We have round ones, diamond-shaped ones, cave-like ones, and long thing path-like ones. We could have rectangular ones, whatever. A Room is just some cells. So far so good.
After all the rooms are generated, we finish the map with three operations:
class DungeonLayout:
def finish_map(self):
self.ensure_connected()
self.make_passages()
self.make_borders()
While it is surely possible for a dungeon-creating function to create a dungeon that is fully connected, we do not require or expect that. Instead, we add additional path-like Rooms connecting Rooms together until you can reach any Room from any other Room. The process for that is iterative: we group rooms that are already connected into Suites, and then we select a cell from each Suite and draw a path, which is a Room. We repeat this until the whole dungeon is connected, in the method ensure_connected.
At this point, each Room is adjacent to some other Room or Rooms. The adjacency could be just one cell, or it could be many, where Room instances abut each other. You’ll have seen that in the pictures in these articles.
Between any two rooms, we want exactly one actual passageway. All the other adjacent cells between them, if any, are to be solid walls. So the make_passages method does that for us, computing the border where one Room abuts another, and selecting one location to leave open, a “passage”.
Then, finally, we have enough information to draw the actual cells of the Rooms, because we choose the tiles of a Room so that they display white walls on all the sides that, well, are walls.
And that sequence is a serious obstacle to something like, say, pausing after creating all the Rooms. If we pause there, no map will display, because we don’t allocate floor tiling until the make_borders method.
In my spike yesterday, I tried not doing ensure connected, but doing the other two steps, which actually sort of worked, at least producing the picture of the disconnected dungeon that we saw yesterday:

But while we can stop there, we cannot really proceed, because we’ve probably run make_passages, and we certainly had to run make_borders, and running them multiple times breaks things and only sort of works.
All this is made a bit more difficult because if we pause to display during DungeonCreation, we need to create a DungeonView, which means running the DungeonViewMaker, and the changes made by the passages and border logic are such that we need to recompute the DungeonView. That may not be too difficult, since the View now comes down mostly to a list of sprites, so if we fully recompute that list, we can probably just replace the one in the existing View and changes will display.
It all comes down to this:
We want an incremental dungeon creation process, such that we can create a view of it at any (reasonable?) point and display it, then continue to any other (reasonable?) point, display again, all the way from early creation, ideally single room creation, down to actually running the game.
However, at this point, dungeon creation is a batch process and necessary details for viewing are created at the end of the process. If we are to do incremental display, we’ll need to be able to do enough of the passage and border making to allow the dungeon to display sensibly. And since upcoming steps will change those results, we’ll need to be able to throw away the old and replace with the new, or to update the old with the new.
Our design really does not contemplate this idea. Whine, whine …
Reflection
Honestly, I did not expect this. And I could delete yesterday’s article and do something else. But I actually want this capability in the Making App, and, aside from the fact that I just wanted some nice easy additive kind of work to do, this is exactly the kind of change that blind-sides us in real development: they ask for something that seems quite reasonable, except that our design specifically said that it could never do that. And now it has to do it anyway.
OK. I’m sure we’ll learn something. It might even be fun. There’s fun to be had everywhere. Let’s think about how we might do this.
Tentative Design Planning
I think we should begin with a quick look at how those three finishing methods, ensure_connected, make_passages, and make_borders operate.
And we’ll hold off on checking ensure_connected, because, I hope, we will not have a need to step through it, though I am probably wrong about that. Anyway the other two methods are more concerning. In fact, I am most concerned about the make_borders method, since it is the one that allocates flooring for us. We’ll start there:
class DungeonLayout:
def make_borders(self):
self.border_map = BorderMap(self)
Well. I certainly didn’t expect that. OK, what is a BorderMap? No. Wait. Our concern is that the DungeonView should have all the necessary sprites to display the dungeon. How does the DungeonViewMaker create those? Maybe we won’t have to care much about the BorderMap, whatever it is.
class DungeonViewMaker:
def __init__(self, dungeon):
self.setup_assets()
self.dungeon = dungeon
self.pub_sub = dungeon.pub_sub
self.keyed_sprites = KeyedSpriteList(arcade.SpriteList())
self.setup()
@property
def view(self):
return DungeonView(self.dungeon, self.pub_sub, self.keyed_sprites)
def setup(self):
self.create_room_sprites()
self.create_content_lists()
def create_room_sprites(self):
for room in self.dungeon.rooms:
view = RoomView(room)
for cell, sprite in view.generate_sprites(self.dungeon.layout):
self.keyed_sprites.add(cell, sprite)
The sprites come from the RoomView, which is created just for that purpose and then dropped.
class RoomView:
def __init__(self, room):
self.room = room
self.cell_sprites = dict()
self.texture_finder = TextureFinder()
def generate_sprites(self, layout):
for cell in self.room:
sprite = self.make_sprite(layout, cell)
self.cell_sprites[cell] = sprite
yield cell, sprite
def make_sprite(self, layout, cell):
texture = self._choose_flooring_texture(layout, cell)
sprite = params.make_adjusted_sprite(cell, texture)
return sprite
def _choose_flooring_texture(self, layout, cell):
borders: BorderList = layout.get_borders(cell)
border_type = borders.border_string()
name = self.texture_finder.full_name(border_type)
return arcade.load_texture(name)
I remember this. The BorderList is a little object that classifies the borders of a cell, and can return a string showing in ENWS order, a letter if there is a wall in that direction and nothing if not. Details don’t matter here. How do we get the borders?
class DungeonLayout:
def get_borders(self, cell):
return self.border_map[cell]
Why are you skipping around like this? It’s very hard to follow!
You’re right, it is hard to follow. Unlike most articles about programming, my articles are not arranged in a neat logical order to show exactly how perfect the design is and how logical our steps are. My articles do my best to show the actual thought sequence that I use to do the work. So there’s a lot of “let’s do this, no not quite it, let’s do that”. That’s what programming — the human way, anyway — is like. My thoughts are jagged, and so are the articles. I regret any confusion caused thereby.
Looks like we may have to look at this BorderMap thing, since it’s used in picking the cell’s flooring. But I have a vague feeling that maybe we can do better. Anyway, what we see from the code above is that the get_borders returns a BorderList from the BorderMap, which appears to be basically a dictionary. Let’s examine that class.
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]
Well, hell. A BorderList, whatever it is, can be created from the layout and the cell. What other code is making use of the BorderMap and why can’t we just change get_borders to create the BorderList live?
The only use of the BorderMap is that we create it and then fetch from it via get_borders. Therefore we can change get_borders thus:
def get_borders(self, cell):
return BorderList(self, cell)
Tests are all green. Let’s run the program just to be sure, since this is view-related. All good. Remove creation of the BorderMap. Remove the references. Change a couple of tests to use get_borders. Remove the class. Commit: remove BorderMap.
So that’s interesting. Now we only have two methods in our finishing code:
class DungeonLayout:
def finish_map(self):
self.ensure_connected()
self.make_passages()
Let’s try something. We’ll remove the call to finish_map from main, and add it as a keystroke we can call, and see what we get.
Here’s the code I needed in KeyPress:
elif symbol == arcade.key.D:
from dungeon_view_maker import DungeonViewMaker
self.dungeon.layout.finish_map()
maker = DungeonViewMaker(self.dungeon)
maker.setup()
self.view.keyed_sprites = maker.keyed_sprites
I just create a new Maker, run it, and replace the existing View’s sprites with the new ones from the new Maker.
And what we get is just what the doctor ordered:


Reflection
Well. So far it seems like I did a lot of whining for nothing, although there’s always value to a little whining. The surprise discovery that the BorderMap wasn’t needed and that a cell can compute its border string at any time, led to removing a stage from the finish_map method, and we found that we can create a view before or after running that method.
It seems almost certain that we could pause after generating each individual room, or each individual path room, if we need to, and the view could draw a reasonable picture of the situation.
What is less clear is how we could manage a pause and then continue on, unless we do some kind of multi-threaded thing, which is definitely out of scope.
Maybe something like this:
- Set up dungeon creation as a ComposedMethod that just calls a series of dungeon creation step methods, room creations, whatever all we want to step among.
- Start the view on an empty dungeon, in “step mode”.
- Using keystrokes or something more clever, step through a table of those creation methods.
I think that would work. There’s still quite a bit of work to be done, but it turns out that our design isn’t incompatible with the need after all.
I was not best pleased when I came in, but I am best pleased now.
See you next time!