Making Suites
Hello, loves!
Going in, the plan is to work on separate suites of connected rooms. I expect some issues. Some design thinking, no new code.
You may recall the BuildStepper, an object that holds a list of dungeon-creating actions and executes them, either all at once, as in normal use, or one by one, to allow developers (me) to view the process of dungeon creation step by step. Then again, you may not, but anyway we ahve that, and yesterday afternoon I spent some time building up a stepper with subregion in it, and then, for a while, struggling with making it display. The difficulty turned out to be trivial, and it only took me a couple of hours to find it. I isolated the problem down to one statement, which was simply assigning Dot a location. If I commented out that assignment, no picture. I finally traced it to this code:
class DungeonView:
def draw_dungeon_pane(self):
player_cell = self.dungeon.player_cell
if player_cell:
self.cameras.scroll_dungeon_cam(self.dungeon.player_cell)
with self.cameras.dungeon_cam.activate():
self.keyed_sprites.draw()
self.draw_adventurer()
self.draw_flood()
If the player_cell is not set, we don’t scroll the camera and, apparently, it’s looking at outer space or something. Let’s fix that. My first thought was to provide an optional center cell with an or on the first line, but we are not assured that that is a valid cell, in the context of subregions. Anyway, I’m scared of that. Let’d look at scroll_dungeon_cam.
class Cameras:
def scroll_dungeon_cam(self, cell):
x_confined = self.confine(cell.x, self.max_x)
y_confined = self.confine(cell.y, self.max_y)
self.dungeon_cam.position = (x_confined, y_confined)
The code above in DungeonView is the only caller of this method, so let’s change its signature to expect x and y:
def scroll_dungeon_cam(self, x, y):
x_confined = self.confine(x, self.max_x)
y_confined = self.confine(y, self.max_y)
self.dungeon_cam.position = (x_confined, y_confined)
And back in the view:
def draw_dungeon_pane(self):
player_cell = self.dungeon.player_cell
x, y = player_cell.xy if player_cell else self._center()
self.cameras.scroll_dungeon_cam(x, y)
with self.cameras.dungeon_cam.activate():
self.keyed_sprites.draw()
self.draw_adventurer()
self.draw_flood()
def _center(self):
return self.dungeon.max_x/2, self.dungeon.max_y/2
That does the trick. Now excuse me for a moment while I remove some of my debugging / exploration / could it possibly be this code. Done, commit: stepper working.
Down to Actual Work
The stepper program looks like this:
def make_build_table(layout, dungeon):
steps = [
# make_corner_rooms,
make_diamond_in_round_room,
bottom_left,
make_a_diamond_room,
make_a_diamond_room,
make_a_diamond_room,
finish,
top_left,
make_a_round_room,
make_a_round_room,
make_a_round_room,
finish,
bottom_right,
make_a_cave_room,
make_a_cave_room,
make_a_cave_room,
finish,
top_right,
make_a_cave_room,
make_a_diamond_room,
make_a_round_room,
finish,
clear_subregions,
finish,
populate,
add_content,
]
return BuildStepper(steps, layout, dungeon)
There are, of course, methods for each of those phrases. Here are a couple just as examples:
def bottom_left(layout, dungeon):
layout.subregion_LRBT(0, 17, 0, 23)
def make_a_diamond_room(layout: DungeonLayout, dungeon):
if origin := layout.unused_cell_or_none():
room = RoomMaker(layout).diamond(number_of_cells=random.choice([25, 41]), origin=origin)
layout.add_room(room)
The RoomMaker does all the layout of the various shapes. Nothing of interest there this morning. I think we need to deal with the finish operation. What we want is that as we build each subregion, we wind up with a connected group of rooms in each subregion. I think that then, we’ll want to populate that particular subregion with the objects and denizens we have designed for that section of the dungeon. After that, we’ll want to leave that section alone … until all the sections are built, at which point we’ll want to create special paths between the sections, specifying which one to connect to which, with no extra connections, so as to control the flow through the sections.
As things stand now, there is way too much going on in finish, and the final paths that get created aren’t quite the thing. Let’s look at finish as a basis for thinking.
main.py
def finish(layout, dungeon):
layout.finish_map()
layout.clear_subregions()
class DungeonLayout:
def finish_map(self):
self.ensure_connected()
self.make_passages()
I’ll spare the details of ensure_connected, which essentially loops, creating suites of connected rooms and then connecting the suites with “path rooms”, which use the flood fill to find a shortest path between chosen cells of the suites being connected. Interesting, but not germane. It’s make_passages that is the issue I’m concerned about, I think:
class DungeonLayout:
def make_passages(self):
self.passages = dict()
for r1, r2 in itertools.combinations(self.rooms, 2):
self.add_passage(self.random_passage(r1, r2))
def random_passage(self, room_1, room_2):
b_d = self.border_dictionary(room_1, room_2)
if not b_d: return None
possibles = []
for cell, neighbors in b_d.items():
for n in neighbors:
possibles.append((cell,n))
return random.choice(possibles)
def add_passage(self, passage):
if passage:
c1, c2 = passage
self.passages[(c1, c2)] = True
self.passages[(c2, c1)] = True
make_passages considers all the rooms in the layout, finds the borders between each pair of rooms, and selects a random cell pair and defines a passage at that location. Let me show you a couple of pictures showing why that’s a problem.
After central room and lower left

After adding in upper right

In the upper picture, note that the exit from the central diamond room is on the left, but in the lower picture, it has moved to the right. A careful look will show that one of the passages in the lower left has also changed. The reason is that every time we “finish”, we draw new paths, and then add passages again, for all the rooms that exist. This process recreates the borders between rooms, and reallocates the passages, typically changing where they were.
Less obvious but also a problem is the fact that the flooring is recalculated every time we add a room. Presently that doesn’t matter but we have discussed having special flooring decisions available to the dungeon designer. Net net net, once we have allocated flooring and passages in some rooms, we’d like them to be sticky, not overridden when next we deal with other rooms.
This is kind of a big deal, because the original design was that we’d lay out the rooms and then pave them and populate them all at once. The original design would have just one finish, populate sequence at the end of the operation.
Design Thinking
At a time like this, I like to head off to the islands and take a vacation for a decade or so. But that’s not really an available option, so instead let’s see if we can think of some changes that might help:
-
Once a connected group of rooms is built and passages committed in them, we might remove them from consideration. But then when we connected them at the end, wouldn’t we still have the same problem?
-
Maybe we convert the group of connected rooms to a single room somehow. Internally it would still have the same boundaries and passages as before but from the outside would not be analyzed again.
-
Maybe the Room object or Suite object needs to be smarter, embodying things like the borders and passages internally. Those are currently dictionaries and lists in Layout, I think.
-
Maybe a room has to know how to create its flooring, and once having done so never does so again. Currently, I think flooring isn’t part of the Room at all, just generated by Layout at some formerly convenient now somewhat troublesome point. If a Room knew how to create its flooring, that would leave the door open for different flooring styles and approaches.
I want to review some code, in hopes of improving these ideas.
The only interesting code in Room is this:
class Room:
def border_with(self, room):
result = set()
for my_cell in self:
for other_cell in room:
if my_cell.manhattan_distance(other_cell) == 1:
result.add(my_cell)
return result
That method finds the room’s border with another room. Like it says on the tin. However … oddly enough … that method is onlyh used in a test. There must be equivalent code elsewhere? Yes, in DungeonLayout:
class DungeonLayout:
def border_dictionary(self, room_1, room_2):
result = dict()
for cell_1 in room_1:
for cell_2 in room_2:
if cell_1.manhattan_distance(cell_2) == 1:
self.add_borders(cell_1, cell_2, result)
return result
def add_borders(self, cell_1, cell_2, result):
self.add_border(cell_1, cell_2, result)
self.add_border(cell_2, cell_1, result)
The border dictionary lets the layout know, for any two cells, whether Dot can move between them, which after some convolution, gets represented in the Layout passages member.
class DungeonLayout:
def has_passage(self, cell, neighbor):
if self.passages.get((cell, neighbor)):
return True
else:
return False
This is used way down in Cell, to help us decide whether Dot can move:
class Cell:
def can_move_to(self, cell):
return cell.room == self.room or self.has_passage(cell)
def has_passage(self, cell):
from dungeonlayout import DungeonLayout
layout:DungeonLayout = self.layout
return layout.has_passage(self, cell)
I just added the import and type declaration because otherwise PyCharm didn’t castch this use of has_passage as referrring to layout, which led it to suggest that I could remove it, which I cannot.
Break Time
After a couple of hours of coding and writing, my brain is full and I want my iced chai. Let’s have a break and let what we’ve seen perk a bit.
It is pretty clear that there is a lot going on down in the works of the layout (and other places) that is going to need unwiring and rewiring. And, just now, the details are more than I can juggle. The good news is that it seems that all the bits are fairly well separated with separatee top-level methods that are presently called at times we don’t want them. But I’m coming to think that mostly we’ll be able to more things around bit by bit until things are done more incrementally.
I think it’ll be wise to assume that we can do that, and to start moving things. Each time we move something, we’ll make the overall problem a bit simpler, and we’ll learn a bit in doing it. It’s possible, I suppose, that we’ll wind up in a corner with no escape, but there’s always reset. And I don’t think that’s likely to happen.
When we keep methods focused on doing just one thing, they tend to be able to be arranged in a different order fairly regularly. We’ll see, but while I am tired, I am also optimistic.
We’ll start slicing and dicing next time. See you then!