More Filtering
Hello, loves!
I sketched some more, and thought some more, about how we can limit dungeon building to just part of the space.
I still have yesterday’s spike code in place, slightly modified since yesterday AM, and sketched some idea in tests. Let’s see what we have and then figure out what we should really do. I hope to have the guts to toss this whole spike. if need be. (Spoiler: Needn’t be! Woot!)
Recall the problem: We’re wanting to build the dungeon in sections, each confined to a subset of the total space available. The idea is that we’ll build a few rooms, connect them, then place in those rooms all the things needed to open a door (yet to be invented) or pass some obstacle (yet to be invented) to get into the next area, rinse repeat.
The current sub-problem is to provide a way to confine selection of cells to use to a subset of the whole space. We wrote some simple code yesterday, I refined it a bit offline, and wrote some tests. I think I’m going too far. Let’s see. We’ll work from the tests, probably one at a time:
class TestSubRegion:
def test_low_region(self):
layout = DungeonLayout(10, 10)
layout.subregion_LRBT(0, 5, 0, 5)
for _ in range(100):
cell = layout.unused_cell_or_none()
assert 0 <= cell.x < 5
assert 0 <= cell.y < 5
This is pretty brute-force as tests go, but we use a new method in the layout, subregion_LRBT to define a rectangular sub-region. Thereafter, all our calls to get an unused cell return cells in that range. Let’s make that test better before we even look at the code. It is possible that we are not correctly checking x or y against the right components, since they are equal. Instead:
def test_low_region(self):
layout = DungeonLayout(10, 10)
layout.subregion_LRBT(0, 5, 6, 10)
for _ in range(100):
cell = layout.unused_cell_or_none()
assert 0 <= cell.x < 5
assert 6 <= cell.y < 10
Better. Now we can be more confident that the x is checking LR and the y checking BT. Let’s see what the code is:
class DungeonLayout:
def __init__(self, max_x=10, max_y=10):
self.max_x = max_x
self.max_y = max_y
self.subregion = None
self.clear_subregions()
self.cells: dict[tuple[int, int], Cell] = dict()
self._create_cells(max_x, max_y)
self.rooms = []
self.passages: dict[tuple[Cell, Cell], bool] = dict()
self.room_map: dict[Cell, Room] = dict()
self.contents: dict[Cell, list[Content]] = defaultdict(list)
def clear_subregions(self):
self.subregion = SubRegion(0, self.max_x, 0, self.max_y)
def subregion_LRBT(self, l, r, b, t):
self.subregion = SubRegion(l, r, b, t)
def _no_access(self, x, y):
return (x,y) not in self.subregion
def _can_access(self, x, y):
return (x,y) in self.subregion
def _at_xy(self, xy):
if self._no_access(*xy):
return None
return self.cells.get(xy, None)
@property
def active_cells(self):
return (cell for cell in self.cells.values() if self._can_access(*cell.xy))
@property
def available_cells(self):
return [cell
for cell in self.active_cells
if cell.is_available]
def unused_cell_or_none(self):
available = self.available_cells
if available:
return random.choice(available)
else:
return None
I kind of went hog-wild since yesterday, with the _no_access and _can_access methods and the helper properties active_cells and available_cells, feeding unused_cell_or_none. Those two properties should be private, I think. In our attempt to maintain excellent habits, let’s change them even if we do toss them later.
@property
def _active_cells(self):
return (cell for cell in self.cells.values() if self._can_access(*cell.xy))
@property
def _available_cells(self):
return [cell
for cell in self._active_cells
if cell.is_available]
def unused_cell_or_none(self):
available = self._available_cells
if available:
return random.choice(available)
else:
return None
So that’s the code supporting that one test, and that produces a suitable dungeon when we build it this way:
main.py
...
make_diamond_in_round_room(layout, dungeon)
layout.subregion_LRBT(0,27, 0, 27)
make_a_cave_room(layout, dungeon)
make_a_round_room(layout, dungeon)
make_a_diamond_room(layout, dungeon)
layout.subregion_LRBT(29,55, 29, 55)
make_a_cave_room(layout, dungeon)
make_a_round_room(layout, dungeon)
make_a_diamond_room(layout, dungeon)
layout.subregion_LRBT(0,55, 0, 55)
finish(layout, dungeon)
populate(layout, dungeon)
add_content(layout, dungeon)
view = DungeonView(dungeon)
...
That gives us a visibly partitioned setup, but before we look at the picture, I’d like to try something, since this is spike code.
make_diamond_in_round_room(layout, dungeon)
layout.subregion_LRBT(0,27, 0, 27)
make_a_cave_room(layout, dungeon)
make_a_round_room(layout, dungeon)
make_a_diamond_room(layout, dungeon)
finish(layout, dungeon)
layout.subregion_LRBT(29,55, 29, 55)
make_a_cave_room(layout, dungeon)
make_a_round_room(layout, dungeon)
make_a_diamond_room(layout, dungeon)
finish(layout, dungeon)
layout.subregion_LRBT(0,55, 0, 55)
finish(layout, dungeon)
populate(layout, dungeon)
add_content(layout, dungeon)
view = DungeonView(dungeon)
I added a couple of finish calls. finish connects up the rooms presently accessible. My hope was that it would build visibly separate sections, and it sort of did, sort of didn’t:

What we see there is nearly what we want but I’m troubled by that double path to the lower left. I’m not surpised: I think that our finish code probably needs to be less destructive of pre-existing structure than it may be at present.
Before we decide what to do, I want to show you some other tests:
def test_contains(self):
sub = SubRegion (5, 10, 50, 60)
assert (5,50) in sub
assert (9, 59) in sub
assert (10, 59) not in sub
assert (9, 60) not in sub
assert (9, 49) not in sub
assert (4, 59) not in sub
This one posited a new object, SubRegion, understanding in and not in. Here is that class:
class SubRegion:
def __init__(self, l, r, b, t):
self.l = l
self.r = r
self.b = b
self.t = t
def __contains__(self, pair):
x, y = pair
return self.l <= x < self.r and self.b <= y < self.t
That’s all it takes. In a spririt of seeking excellence, I’d probably suggest that the parameters should be made keywords, not just four random names in a row.
The idea here is that instead of just having a handful of member variables inside DungeonLayout, we could use the subregion class as shown in the layout code above.
Then I started getting fancy, and I think someone should stop me. I have some drawings of potential layouts on cards, and I went well beyond simple rectangles.

So I starting thinking about combining rectangular subregions to cut out spaces and add in spaces, and created this test and class:
def test_and(self):
sub = SubRegion (5, 10, 50, 60)
sub2 = SubRegion (7, 8, 50, 60)
combined = AndRegion([sub, sub2])
assert (7, 55) in combined
assert (8, 55) not in combined
class AndRegion:
def __init__(self, subs):
self.subs = subs
def __contains__(self, c):
return all(c in sub for sub in self.subs)
It seems to me now, in the clear light of day, that that idea is marginally useful at best, and that using it directly would not be at all communicatiive to the programmers. Then, if that wasn’t insane enough, I sketched this:
pytest.mark.skip('wait')
def test_fluent(self):
region = Region()\
.and_(5, 10, 50, 60)\
.and_(6, 15, 55, 65)\
.or_(100, 110, 0, 50)
The idea being wouldn’t it be nice to build up the subregion with ands and ors and xors and I don’t know NAND gates and probalby hyperbolic cotangents or something? Right, it wouldn’t. While those are interesting ideas, simple rectangles can approximate anything we’re likely to really want to do for a long time. I think it we leave the SubRegion idea in place, we have an object that’s doing the job of checking that we’re in a suitable subregion, and if we ever need more, we can elaborate SubRegion to be as intelligen as we like, without the need to change anything else.
So. What should we keep, what should we ditch? I think we should ask PyCharm what’s in our diff? Not that much … the new test file, which is harmless to keep. In DungeonLayout, just what I’ve shown you, and that code demonstrably works. In ‘main’, I based in that code to let me see what it looks like, and that should go. Let’s revert main and save the other two files.
Commit the other two files: new working subregion idea.
Summary
In a brief session yesterday afternoon, I did a few things. One is that I refactored the morning’s spike to decent code, including a new simple object to cover the new behavior. I even did that with tests, yay me! Then I did a couple of experiments, in the test file, experimenting with logical operators and sketching what a fluent interface might look like.
The changes to DungeonLayout are pretty clean and I decided to let them live. The changes to ‘main’ were very ad-hoc and reverted. The test file is either testing real code, or isolated experimental code inside the test, so that can be committed.
Bottom line, I think the current very simple SubRegion idea is sufficient to let us work on the next part of the feature, building up isolated sections in the dungeon, and connecting them such that we can be sure that all the necessary items are in the section they need to be in, and the sections are connected by “obstacle paths” (hmm, having coined the phrase, maybe there is an object there), and not connected by other paths that would let you get around a puzzle.
In short, I think we’re decentl positioned for the next phase, which we’ll figure out next time. See you then!