Back on Track
Hello, loves!
Let’s get back to work on displaying the path to things. We need a different Flooder setting for sure.
The current scheme for finding the path between Dot and some named treasure shortcuts through walls. I suppose that might be interesting,, requiring Dot to explore to find the trail again, but what we had in mind was that it would draw a legitimate path. We have that technology, so let’s see about plugging it in. ALong the way, I think we’ll find a better place for the code.
At present we trigger a path from Dot to ‘a red key’ by typing a K. In actual use, the path will be triggered by other actions, to be defined.
class DungeonView:
elif symbol == arcade.key.K:
self.dungeon.show_path_to('a red key', self)
class Dungeon:
def show_path_to(self, item_name, dungeon_view):
my_resources = '/Users/ron/Desktop/DungeonTiles/png/objects/'
item = ContentFactory().decor(name="skel", resource=my_resources + 'Skeleton1.png', scale=0.5)
path = self.find_path_to(item_name)
for cell in path:
self.place_content_at(cell, item)
dungeon_view.make_view_and_sprite(cell, item)
def find_path_to(self, item_name):
target = self.find_cell_containing(item_name)
if target:
path = self.layout.find_path(target, self.player_cell, self.rooms)
return path[1:-1]
else:
return []
def find_cell_containing(self, item_name):
cell = None
for candidate, items in self.contents.items():
for item in items:
if item.name == item_name:
cell = candidate
return cell
class DungeonLayout:
def find_path(self, source, target, room_list):
def can_use(cell, _parent):
return self.is_available(cell) or self.get_room(cell) in room_list
reached = self.path_map(source, can_use, 1.0)
path = []
start = target
while start is not None:
path.append(start)
start = reached.get(start)
return path
def path_map(self, source, can_use, randomness=0.0):
reached: dict[Cell, Cell|None] = {source: None}
for cell, parent in (Flooder(layout=self, origin=source)
.select(can_use)
.initial_value(None)
.next_value(lambda c, _v: c)
.randomness(randomness)
.flood()):
reached[cell] = parent
return reached
The reason that find_path is taking shortcuts is that its can_use selection criterion allows cells from any room, without regard to details like walls.
I think we already have a better option built into Flooder:
def can_traverse(self):
self.select(lambda cell, parent: parent.can_move_to(cell))
return self
Right. I’m curious how the existing find_path might be used. Its only other use in prod is in the creation of paths between Suites, which we do allow to cut straight across, although I suspect it never happens.
- A small problem.
- It’s “easy” to see that we could make two find_path entry points, and have them pass in either the current
can_useor thecan_traverseequivalent, but we can’t plug in a convenience method. OK, we’ll do it the hard way.
Back in DungeonLayout, extract a method to make room for a different version:
class DungeonLayout:
def find_path(self, source, target, room_list):
def can_use(cell, _parent):
return self.is_available(cell) or self.get_room(cell) in room_list
return self.find_path_with_condition(source, target, can_use)
def find_path_with_condition(self, source, target, can_use):
reached = self.path_map(source, can_use, 1.0)
path = []
start = target
while start is not None:
path.append(start)
start = reached.get(start)
return path
Now do find_traversable_path:
class DungeonLayout:
def find_traversable_path(self, source, target):
def can_use(cell, parent):
return parent.can_move_to(cell)
return self.find_path_with_condition(source, target, can_use)
Now use that in our breadcrumbs code.
class Dungeon:
def find_path_to(self, item_name):
target = self.find_cell_containing(item_name)
if target:
path = self.layout.find_traversable_path(target, self.player_cell)
return path[1:-1]
else:
return []
I think our breadcrumbs should now route around walls.

And there it is. I couldn’t resist trying a harder one:

Reflection
So that works as intended, with details yet to be added, which we’ll do in a later session. There are things not to like in this code, and while we’ll commit this: K path no longer walks through walls, we should do some cleaning up. Issues include:
-
We have been testing this in the viewer and should ideally have a better test for it. Arguing against that: it would be a tedious story test and what we have seems solid. Arguing in favor, I have no idea what it’ll do if the object is not accessible. Currently we have no real way to test that other than to build a room with no doors.
-
Building the path inside Flooder seems like a reasonable thing to do, instead of having code lying about building path-maps and such. That might be kind of fun, given the fluent interface to Flooder.
-
It would be ideal if all the various kinds of select conditions were encapsulated in Flooder, so that unless someone is doing something truly bizarre, they would never have to write them.
-
With that in mind … with a little more duplication in what we just did, maybe we could fold the select criteria into Flooder.
I’m glad we thought about these things. Let’s chase that last idea a bit.
We have this now, in DungeonLayout:
class DungeonLayout:
def find_path(self, source, target, room_list):
def can_use(cell, _parent):
return self.is_available(cell) or self.get_room(cell) in room_list
return self.find_path_with_condition(source, target, can_use)
def find_traversable_path(self, source, target):
def can_use(cell, parent):
return parent.can_move_to(cell)
return self.find_path_with_condition(source, target, can_use)
def find_path_with_condition(self, source, target, can_use):
reached = self.path_map(source, can_use, 1.0)
path = []
start = target
while start is not None:
path.append(start)
start = reached.get(start)
return path
def path_map(self, source, can_use, randomness=0.0):
reached: dict[Cell, Cell|None] = {source: None}
for cell, parent in (Flooder(layout=self, origin=source)
.select(can_use)
.initial_value(None)
.next_value(lambda c, _v: c)
.randomness(randomness)
.flood()):
reached[cell] = parent
return reached
We presently have no other use for the pathmap than to create a path from it. But I think there are other uses for them. For example, using a pathmap, we could have a number of creatures advancing inexorably toward Dot’s location, closing in, closer and closer, chanting eldritch curses at which the human mind clutches but is unable to grasp, until ultimately … well you get the idea.
Anyway, it seems to me that Flooder could be offering a few convenient path-making services. That would offload complexity from places like Dungeon and DungeonLayout, and that would be a good thing.
Let’s try something like this. We’ll change:
class DungeonLayout:
def find_path(self, source, target, room_list):
def can_use(cell, _parent):
return self.is_available(cell) or self.get_room(cell) in room_list
return self.find_path_with_condition(source, target, can_use)
def find_path(self, source, target, room_list):
return Flooder(layout=self,origin=source).path_through_rooms(target, room_list)
Typing that in breaks ten tests, very good news, we’ll have lots of confidence when they all work again.
No. Belay that. Gets too weird. We’ll have a new class associated with Flooder. Call it PathFinder, since we got rid of the old class of that name?
def find_path(self, source, target, room_list):
return PathFinder(self, source, target).using_rooms(room_list)
Then, basically, it’s copy-pasta and a bit of editing. I probably could have done the forwarding back and forth trick but this was easy enough:
class PathFinder:
def __init__(self, layout, source, target):
self._layout = layout
self._source = source
self._target = target
def using_rooms(self, rooms):
def can_use(cell, _parent):
return self._layout.is_available(cell) or self._layout.get_room(cell) in rooms
return self.find_path_with_condition(can_use)
def find_path_with_condition(self, can_use):
reached = self.path_map(can_use, 1.0)
path = []
start = self._target
while start is not None:
path.append(start)
start = reached.get(start)
return path
def path_map(self, can_use, randomness):
reached: dict[Cell, Cell | None] = {self._source: None}
for cell, parent in (Flooder(layout=self._layout, origin=self._source)
.select(can_use)
.initial_value(None)
.next_value(lambda c, _v: c)
.randomness(randomness)
.flood()):
reached[cell] = parent
return reached
Commit: new PathFinder supporting find_path in DungeonLayout.
Now let’s replace the other use of the DungeonLayout version:
def find_traversable_path(self, source, target):
return PathFinder(self, source, target).traversing()
And, almost trivially:
class PathFinder:
def traversing(self):
def can_use(cell, parent):
return parent.can_move_to(cell)
return self.find_path_with_condition(can_use)
The skull path works. We still don’t have a test for that, I believe.
We can remove all that code from DungeonLayout. Commit: PathFinder handling all DungeonLayout’s path finding needs.
Reflection
We have removed more than 25 lines of DungeonLayout that had nothing to do with its primary function, namely to know the cells and rooms of the dungeon. It’s down from a portly 242 lines to a more trim 215. Still a ways to go, I’m sure.
I built the PathFinder inside the flooder file. I’m not sure why, I just made that choice. But we really like one class per file unless one of them is truly private to the other. So I’ll break it out. PyCharm handles that with a keystroke. Commit: PathFinder in its own file.
I think we might be done here. We’ve done some good stuff and the code seems improved. Oh, that reminds me:
You may not have noticed that the PathFinder version of the code is simpler, with fewer parameters, than the version inside DungeonLayout. That’s a direct result of the PathFinder being created with the relevant parameters as member variables, so that they can be accessed as instance variables, rather than passed around as parameters. This is, of course, another sign that the path finding didn’t really belong in DungeonLayout.
Isn’t there some kind of finding thing in Dungeon? I seem to recall that there is.
Ah yes, that’s where our K thing starts:
class Dungeon:
def show_path_to(self, item_name, dungeon_view):
my_resources = '/Users/ron/Desktop/DungeonTiles/png/objects/'
item = ContentFactory().decor(name="skel", resource=my_resources + 'Skeleton1.png', scale=0.5)
path = self.find_path_to(item_name)
for cell in path:
self.place_content_at(cell, item)
dungeon_view.make_view_and_sprite(cell, item)
def find_path_to(self, item_name):
target = self.find_cell_containing(item_name)
if target:
path = self.layout.find_traversable_path(target, self.player_cell)
return path[1:-1]
else:
return []
def find_cell_containing(self, item_name):
cell = None
for candidate, items in self.contents.items():
for item in items:
if item.name == item_name:
cell = candidate
return cell
We could inline that call to layout, I reckon. PyCharm thinks it can do it, but it can’t. However, I can:
def find_path_to(self, item_name):
target = self.find_cell_containing(item_name)
if target:
path = PathFinder(self.layout, target, self.player_cell).traversing()
return path[1:-1]
else:
return []
Now I reckon we can remove that traversing method from layout. Yes. Commit: use pathfinder in dungeon show path to.
It’s really time to break, there is breakfast and the Tour in the offing. But I’m wondering what other uses we’re making of Flooder and whether we can improve them, so we’ll at least take a look.
Dungeon has the maker_flood method which just shows all the distances from Dot. I’m not sure we need that any more but I recall it was useful a while back.
DungeonLayout uses it to find the cells furthest from a given point. That is intended to be used to place Dot in the worst possible location. The Layout also uses Flooder to define Suites which we use to ensure that the dungeon is fully connected.
RoomMaker / RoundRoomCollector uses it to fill a “round” room, and DiamondRoomCollector uses it similarly. And 14 test use it.
I think we’re in good shape. Let’s sum up.
Summary
Changing our path finding code to use available cells was easy enough. We did a simple extract method to provide a find method that accepted alternative cell selection criteria and then used it. Then we looked at the path finding code in Layout and it clearly didn’t belong there.
I at fist though to provide class methods on Flooder but a quick sketch of that showed me that I didn’t like that, so we did a new helper object, PathFinder, which has the same capability as the code in DungeonLayout, but smaller, simpler, and entirely cohesive. And we removed the corresponding code from the Layout, making it more cohesive (and smaller) as well.
It’s like that time my pal Jake transferred from Heelan to Central High, increasing the average IQ of both schools.
So, a good morning’s work. Still need better testing of some of this. I might have to think harder than I like to about that. We’ll see. See you next time!