Refactoring = Learning
Hello, loves!
While you were away, I did a little refactoring. Let’s see what it shows us. I think we’ve done some good here.
I’m still working on paths between sections, and still doing most of the work in the tests. Mostly. Yesterday’s afternoon session, unpublished, made a change to the Passages object.
Here’s what our active test looked like when last we met:
layout.clear_subregions()
path_cells = suite_1.find_extended_path(suite_2)
# now path_cells[0] is in one suite, and
# path_cells[-1] in the other, and
# path_cells[1:-1] are the cells we need for
# the new path room.
room_2 = path_cells[0].room
assert room_2 in suite_2.room_set
room_1 = path_cells[-1].room
assert room_1 in suite_1.room_set
path_room_1_2 = Room(path_cells[1:-1])
layout.add_room(path_room_1_2)
layout.make_passages()
assert path_cells[0].has_passage(path_cells[1])
assert path_cells[-1].has_passage(path_cells[-2])
yield
And now …
layout.clear_subregions()
path_cells = suite_1.find_extended_path(suite_2)
# now path_cells[0] is in one suite, and
# path_cells[-1] in the other, and
# path_cells[1:-1] are the cells we need for
# the new path room.
room_2 = path_cells[0].room
assert room_2 in suite_2.room_set
room_1 = path_cells[-1].room
assert room_1 in suite_1.room_set
path_room_1_2 = Room(path_cells[1:-1])
layout.add_room(path_room_1_2)
# layout.make_passages()
layout.passages.add_passage_unconditionally((path_cells[0], path_cells[1]))
layout.passages.add_passage_unconditionally((path_cells[-1], path_cells[-2]))
layout.passages.isolate(path_room_1_2)
assert path_cells[0].has_passage(path_cells[1])
assert path_cells[-1].has_passage(path_cells[-2])
yield
The difference is there near the end. Instead of using make_passages, we use the extended path—the path between rooms that includes the starting and ending cells, in the rooms we’re connecting. We know we want the first two cells in that path to be a passage to the one room, and the last two to be a passage to the other room. So we add those two paths directly. The unconditionally method already existed.
THen we call layout.passages.isolate, which is a new method that makes the final path impermeable. That required some new code:
class Passages:
def __init__(self):
self.cell_connections: set[tuple[Cell, Cell]] = set()
self.room_connections: set[tuple[Room, Room]] = set()
self.isolated: set[Room] = set()
def isolate(self, room: Room):
self.isolated.add(room)
And we changed this:
def create_passage(self, r1: Room, r2: Room, func):
if r1 in self.isolated or r2 in self.isolated:
return False
elif self.rooms_are_connected(r1, r2):
return False
else:
passage = func()
self.add_passage_unconditionally(passage)
return True
It’s just the first two lines that were added. If either of the two rooms to be connected is isolated, we return False without creating a passage. If not, then as before we don’t connect them if they are already connected, and if they aren’t connected, we call the provided function and connect them.
Your point, sir?
The point of writing the code yesterday, if I recall, was that calling make_passages is kind of a big deal, with a lot of magic going on, and in the case in hand, we know more precisely what needs to be done, so basically I wanted to work out just what we needed and do that.
So the operations we need to connect two sections are now in the test and, cleaned up, they go like this:
layout.clear_subregions()
path_cells = suite_1.find_extended_path(suite_2)
path_room_1_2 = Room(path_cells[1:-1])
layout.add_room(path_room_1_2)
layout.passages.add_passage_unconditionally((path_cells[0], path_cells[1]))
layout.passages.add_passage_unconditionally((path_cells[-1], path_cells[-2]))
layout.passages.isolate(path_room_1_2)
That sure looks like code that belongs in DungeonLayout, doesn’t it?
I’d like to move it over there, and I’d like not to lose the value of this test, which is looking at some details that will be hidden in the method we extract. We can’t be sure that the code will find the same path every time. If there is more than one pair of “closest” cells, the starting and ending points of the path are random. Maybe if we seed the random number generator?
I think we’ll replicate this test except for calling our new method and see what we can see. And we will seed the RNG and use the same seed in both tests. Here’s the steps method in the new test:
def steps():
random.seed(14159)
layout.subregion_LRBT(0, 20, 0, 20)
new_rooms = make_diamond_section()
suite_1 = Suite(layout, new_rooms)
layout.subregion_LRBT(0, 20, 35, 55)
new_rooms = make_diamond_section()
suite_2 = Suite(layout, new_rooms)
layout.clear_subregions()
path_cells = suite_1.find_extended_path(suite_2)
layout.add_isolated_path(suite_1, suite_2)
assert path_cells[0].has_passage(path_cells[1])
assert path_cells[-1].has_passage(path_cells[-2])
# note: test relies on our path_cells being
# same as the path found in `add_isolated_path`
# currently Suite.find_closest_pair is
# deterministic, so this works.
yield
To allow for some checking, we privately call find_extended_path, which will return the same path that the add_isolated_path method uses, so we can check the ends to find anything we want to know.
The new method is as one would expect:
class DungeonLayout:
def add_isolated_path(self, s1: Suite, s2: Suite):
x_path = s1.find_extended_path(s2)
path_room = Room(x_path[1:-1], 'isolated path')
self.add_room(path_room)
self.passages.add_passage_unconditionally((x_path[0], x_path[1]))
self.passages.add_passage_unconditionally((x_path[-1], x_path[-2]))
self.passages.isolate(path_room)
Let’s commit this: layout can create isolated path between suites.
Now I want to try this new scheme in an actual dungeon build. That’s going to be a bit messy, and it’ll probably give us some ideas for improvement. I’ll just edit the existing dungeon build.
Somewhat tedious but it goes like this:
def yield_table_steps(layout, dungeon):
bottom_left(layout)
make_a_diamond_room(layout)
make_a_diamond_room(layout)
make_a_cave_room(layout)
make_a_diamond_room(layout)
make_a_diamond_room(layout)
finish(layout)
suite_bl = Suite(layout, set(layout.rooms))
yield
top_left(layout)
previous_rooms = set(layout.rooms)
make_a_round_room(layout)
make_a_round_room(layout)
make_a_round_room(layout)
make_a_round_room(layout)
make_a_round_room(layout)
finish(layout)
suite_tl = Suite(layout, set(layout.rooms) - previous_rooms)
yield
clear_subregions(layout)
layout.add_isolated_path(suite_bl, suite_tl)
yield
top_right(layout)
previous_rooms = set(layout.rooms)
make_a_cave_room(layout)
make_a_diamond_room(layout)
make_a_cave_room(layout)
make_a_cave_room(layout)
make_a_round_room(layout)
finish(layout)
suite_tr = Suite(layout, set(layout.rooms) - previous_rooms)
yield
clear_subregions(layout)
layout.add_isolated_path(suite_tl, suite_tr)
yield
bottom_right(layout)
previous_rooms = set(layout.rooms)
make_a_cave_room(layout)
make_a_diamond_room(layout)
make_a_cave_room(layout)
make_a_diamond_room(layout)
make_a_cave_room(layout)
finish(layout)
suite_br = Suite(layout, set(layout.rooms) - previous_rooms)
yield
right_side(layout)
layout.add_isolated_path(suite_tr, suite_br)
yield
clear_subregions(layout)
previous_rooms = set(layout.rooms)
make_diamond_in_round_room(layout)
bottom_right_plus_center(layout)
finish(layout)
suite_boss = Suite(layout, set(layout.rooms) - previous_rooms)
yield
clear_subregions(layout)
layout.add_isolated_path(suite_br, suite_boss)
populate(layout, dungeon)
yield
add_content(layout, dungeon)
yield
There’s a pattern there, where we save the previous rooms, create our new suite from all the rooms, and we’ll look at that next time to see how to make the job easier, but as written we get a map that looks good to me:

Between that and our tests, I think we’re happy enough. Commit the main. Let’s sum up.
Summary
We’ve seen a nice flow of events here. We started with some simple tests, built up to a pretty complex one, then expanded the internals of the test to see that a new method on DungeonLayout could do the job, then created a new test calling that method, wrote the method, bada bing bada boom.
Things went as smoothly as they ever do, and the result, the add_isolated_path facility seems to be just what we need.
The pattern in the main program, where we save all the existing rooms, build some stuff, make a suite of it, and use that suite later, seems worthy of packaging somewhere, probably as a function to use in building.
I suspect that we should begin to abstract building a bit, since (if this were a real game) there would be many dungeon layouts to be done and there should be some way of organizing them. Since we’re here to do things that interest me, we’ll do that if it seems interesting.
So. A productive afternoon and morning. Now to make a nice iced chai and find something to read. See you soon!