Hello, loves!

Depressed by lack of internet. Going to try a idea. (Long depressing debacle. Kept for the record, probably no fun to read.)

Saturday AM

Over 40 hours on generator, still no internet. I have a very low social need but even I am feeling isolated. Might have to trek out in search of wifi. I might be able to get a couple of bars on the phone if I were to go stand in the yard like some kind of madman.

In thinking about the made-up problem of building a dungeon in lightly-connected sections, I’m beginning to think that the current design is not capable of doing what we need. It was built to do the job of creating flooring and content sprites all in one go. As such, it doesn’t record much detail at all. During play, all the dungeon really needs to know is whether Dot can move to a given adjacent cell, and what content items are in her current or proposed location. Those decisions are made by looking at Cell instances to see if they are in the same room, or constitute a passage between one room and another. And the DungeonLayout object has a dictionary from Cell instance to a list of Content.

According to the original design, after all the Rooms have been created, there is a pass made to ensure that they are all connected, so that there are no isolated islands. (I think it is possible that a weird enough layout could create an island, but I’ve not seen it happen with the present scheme.)

A Cell’s Content is defined explicitly during a dungeon build. It could be random, carefully crafted, whatever. It could be done incrementally per room if desired, or at any time. At present, it is done after the rooms are all connected.

The DungeonView was written with the expectation that the dungeon would be ready to run before the view was created. When we added the BuildStepper with its ability to build one step at a time, this expectation no longer holds, and therein lies our concern.

To display the dungeon flooring, walls, and content, the View must have two SpriteList instances, the flooring sprites and the content sprites. These are created by the KeyedSpriteListMaker, which hands the lists to the View when they are done. The process of making them amounts to scanning all the rooms of the dungeon, determining where the walls are, and selecting floor tiles that look right. There are tiles with no borders, for regular flooring, and tiles with walls on one or more sides, as needed. The KeyedSpriteList maker uses a RoomView object for each room in its purview, and the RoomView generates the flooring. There is some randomness in the generation, because there is more than one tile that can serve a given purpose and the RoomView picks a suitable tile randomly. The actual selection of the tile is in a subordinate object, TileFinder, which I think we can ignore.

So. If we are stepping the dungeon build so as to observe it, one issue is that after each step we must have all the cells associated with floor tiles. The Stepper does this after every step:

    def _do_one_step(self, view):
        self.table[self.index](self.layout, self.dungeon)
        if view:
            sub = view.dungeon.layout.subregion
            view.dungeon.layout.clear_subregions()
            view.update_KSL()
            view.dungeon.layout.subregion = sub
        self.index += 1

Hm. Does this code mean that stepper is always creating and recreating the sprite lists? That would be bad and fixing it might help us out in our current quest.

I commented out the update. The window comes up much faster. The BuildStepper accounts for a lot of the slowth of startup. I would not have guessed that had we not had this little chat.

Let’s rewire things a bit. Currently, the BuildStepper calls _do_one_step when we run its finish method, which is intended to just spin through all the build, in the normal game mode. The D key press calls step. Let’s have a look at this:

class BuildStepper:
    def __init__(self, table, layout, dungeon ):
        self.table = table
        self.index = 0
        self.layout = layout
        self.dungeon = dungeon

    def finish(self, view=None):
        while self._more_to_do():
            self._do_one_step(view)
        if view:
            view.update_KSL()

    def step(self, view=None):
        if self._more_to_do():
            self._do_one_step(view)
        return self._more_to_do()

    def _more_to_do(self):
        return self.index < len(self.table)

    def _do_one_step(self, view):
        self.table[self.index](self.layout, self.dungeon)
        if view:
            sub = view.dungeon.layout.subregion
            view.dungeon.layout.clear_subregions()
            view.update_KSL()
            view.dungeon.layout.subregion = sub
        self.index += 1

I just added the if view: to finish, because with the other one not in place, we need to update_KSL or it will never happen. The step method is only used in KeyPress. Let’s give it a better name, maker_single_step and refactor a bit:

class BuildStepper:
    def __init__(self, table, layout, dungeon ):
        self.table = table
        self.index = 0
        self.layout = layout
        self.dungeon = dungeon

    def finish(self, view=None):
        while self._more_to_do():
            self._do_one_step(view)
        if view:
            view.update_KSL()

    def maker_single_step(self, view=None):
        if self._more_to_do():
            self._do_one_step(view)
            self._update_view(view)
        return self._more_to_do()

    def _update_view(self, view):
        if view:
            sub = view.dungeon.layout.subregion
            view.dungeon.layout.clear_subregions()
            view.update_KSL()
            view.dungeon.layout.subregion = sub

    def _more_to_do(self):
        return self.index < len(self.table)

    def _do_one_step(self, view):
        self.table[self.index](self.layout, self.dungeon)
        self.index += 1

The changes are:

  1. _do_one_step does not update the KSL;
  2. finish updates the KSL only when the build is complete;
  3. maker_single_step updates KSL always.
  4. As before, no one updates the view if it is None, which it can be for testing.

Commit: improve BuildStepper, dungeon build much faster due to not calling update_KSL a million times.

Reflection

We’re still just kind of circling around the real issue, which has to do with updating KSL at all, as well as related issues. We’ll get back to that shortly. However, what we’ve done this morning makes the system work more the way it should work, only updating the KSL when it needs to be updated, that is, just before we need to see the dungeon. Changes like this don’t solve our original problem, but they make solving it easier, in two regards.

First, ensuring that things are only done as needed will reduce the places where something could go wrong, introducing a defect or just making our job harder. Second, by doing things only as needed, in the way that is needed, we simplify the code and reduce our mental load. Third—I was mistaken about the two—making these changes helps embed the way things work in our mind, making it easier to work out next steps. All three of these are of course related, in that they are all aspects of preparing the code and our mind to have the same understanding of what’s going on.

Design Thinking

As for our problem, there are still at least two aspects that need to be addressed:

We need to settle the flooring and borders once and for all, because, in principle, a room’s definition might include specific flooring decisions. It is possible that we can finesse this one, but it is irritating to see the flooring changing every time we step.

In addition, we want to connect each section independently, and then only later connect all the sections. As connection works now, we build connected Suites of rooms and then connect them. It follows that if the individual sections are connected, the Suite logic at the end should connect them up. However, the current algorithm always builds a passage between two rooms if they have adjacent cells. Take a look at this map, for example:

map showing many passages

The three rooms in this view are in the first subregion that we created. You can see that they happen to have been fully connected as created, since they were adjacent one to another. But when we drew the paths to connect them with the rest of the dungeon, the hallways were created, and in every place where one room (hallway) touched another, we added a passage between them. Up until now, I had just considered that to be interesting and to make the dungeon more interesting, since with limited lighting you wouldn’t know what to expect: an opening could lead to something interesting, or not.

But with the subregion idea, we want to control the passages. So we’re going to need a new kind of passage, one that will not make extra passages between things they happen to touch.

One idea I have for making things better is to keep a collection of Room instances that have already been laid out with passages, as a group of Rooms that are locally connected, and then when we run KSL again, to skip those rooms. That would leave them pristine as first created. Then, we’d need another kind of path between rooms, one that doesn’t create extra passages, just passages at the end points.

Let’s try that as a spike. We’ll have a collection somewhere … the KeyedSpriteListMaker does this currently:

class KeyedSpriteListMaker:
    def create_room_sprites(self, dungeon, keyed_sprites):
        for room in dungeon.rooms:
            view = RoomView(room)
            for cell, sprite in view.generate_sprites(dungeon.layout):
                keyed_sprites.add(cell, sprite)

What if it could say this, via Wishful Thinking:

    def create_room_sprites(self, dungeon, keyed_sprites):
        for room in dungeon.unfloored_rooms:
            view = RoomView(room)
            for cell, sprite in view.generate_sprites(dungeon.layout):
                keyed_sprites.add(cell, sprite)
            dungeon.mark_floored(room)

Then we’d just do them once. That seems nearly good. Dungeon doesn’t know the rooms, it forwards to the layout.

class Dungeon:
    @property
    def unfloored_rooms(self):
        return self.layout.unfloored_rooms

And

class DungeonLayout:
    @property
    def unfloored_rooms(self):
        return self.rooms

And if we add these

class Dungeon:
    def mark_floored(self, room):
        self.layout.mark_floored(room)

class DungeonLayout:
    def mark_floored(self, room):
        pass

All our tests pass and the game will run as before. Commit: working on not reflooring floored rooms.

Wait, what? You didn’t DO anything!

Oh, but I did. I made a place for the flooring logic to stand. This technique, or one much like it, was named “Fake It Till You Make It” by Kent Beck.

Now we can implement the feature inside DungeonLayout, without disturbing any other classes.

My first attempt nearly works:

class DungeonLayout:
    def __init__(self, max_x=10, max_y=10):
        ...
        self.rooms = []
        self.floored_rooms = []
        self.passages: dict[tuple[Cell, Cell], bool] = dict()
        self.room_map: dict[Cell, Room] = dict()
        self.contents: dict[Cell, list[Content]] = defaultdict(list)

    @property
    def unfloored_rooms(self):
        return [room for room in self.rooms if room not in self.floored_rooms]

    def mark_floored(self, room):
        self.floored_rooms.append(room)

I say ‘nearly’ because a test fails and so does the game. Here’s the test failure:

../src/keyed_sprite_list.py:38: in illuminate
    sprite = self.flooring_sprite_at(cell)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <keyed_sprite_list.KeyedSpriteList object at 0x1154b4e30>
cell_or_item = Cell(12, 10)

    def flooring_sprite_at(self, cell_or_item):
>       return self.sprite_dict[cell_or_item]
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E       KeyError: Cell(12, 10)

../src/keyed_sprite_list.py:32: KeyError

I think what’s happening may be that we are clobbering the KeyedSpriteList in DungeonView with a new one, and the new one will be incomplete because we aren’t redoing all the flooring. I’m not sure, however. Best figure out the test, which is rather long:

    def test_room_view_illuminate(self):
        layout = DungeonLayout(20,20)
        dungeon = Dungeon(layout)
        cells = [Cell(x,y) for x in range(9,15) for y in range(9,15)]
        room = Room(cells, layout)
        layout.add_room(room)
        layout.finish_map()
        dungeon.just_set_player_position(Cell(12,12))
        dungeon_view = DungeonView(dungeon)
        dungeon_view.update_KSL()
        dungeon_view.illuminate_around_dot()
        lit_cell = Cell(13, 13)
        lit = dungeon_view.keyed_sprites.flooring_sprite_at(lit_cell)
        assert lit.visible == True
        unlit_cell = Cell(9, 9)
        unlit = dungeon_view.keyed_sprites.flooring_sprite_at(unlit_cell)
        assert unlit.visible == params.sprite_visible

Yes. These two lines:

        dungeon_view = DungeonView(dungeon)
        dungeon_view.update_KSL()

update_KSL is called twice there since creation of DungeonView calls it (it also throws away the result, which is something we’ll need to fix). Since KeyedSpriteListMaker presently creates new sprite lists, and fills in the cells, and since the first call gets the room marked as floored, the tables are not properly up to date. I suspect that the sprite lists are wrong as well: I don’t see how they could be correct.

I’m tired and my eyes are dry. I’m going to reset that update and get back to the version that does nothing.

I doubt sincerely whether I’ll publish this sequence at all. It is long and very boring.

Later That Day …

Commit: Updated KSLM to KeyedSpriteListUpdater, now updates the same list every time. Minor mods to DungeonView. Implemented unfloored_rooms in DungeonLayout. Present scheme makes no passageways, we’ll have to fix that. Added finish calls in main, probably needs to be a different method but we’ll see.

Sunday …

As currently rigged up, we’re getting no passageways shown on the map in single-step mode, but they do show up when we’re not stepping. What’s the difference?

class BuildStepper:
    def maker_single_step(self, view=None):
        if self._more_to_do():
            self._do_one_step(view)
            self._update_view(view)
        return self._more_to_do()

    def _update_view(self, view):
        if view:
            sub = view.dungeon.layout.subregion
            view.dungeon.layout.clear_subregions()
            view.update_KSL()
            view.dungeon.layout.subregion = sub

The issue, of course, is the update_KSL, which moves rooms to the floored_rooms collection as soon as is applies flooring. We have not, at that time, defined any passages, so there are none. We can improve this situation slightly by building the passages before we do the KSL update.

No. The bug is allowing the BuildStepper to take any action that changes the dungeon. But if we don’t assign any flooring, we won’t get any picture. Let’s refine that statement: the BuildStepper can take no action that permanently changes the dungeon.

What about this idea? What if we change the view so that it displays a default flooring texture for any cell that has None? That would resolve part of the problem: it would make the layout visible. It would not, however, show us the passageways. Maybe that’s OK.

Let’s see if we can make that happen. Ah. We really cannot: We only display the KeyedSpriteLists, all in one go, to keep arcade happy. So if we are to see the dungeon, we need to put something in the KSL. Could we later remove those things?

Review

I need to refresh my mind on the flow of things when we’re not stepping, because it is mostly what we want, or, if it isn’t it is what we have.

It all kind of happens in one go:

class KeyedSpriteListUpdater:
    def create_room_sprites(self, dungeon, keyed_sprites):
        for room in dungeon.unfloored_rooms:
            view = RoomView(room)
            for cell, sprite in view.generate_sprites(dungeon.layout):
                keyed_sprites.add(cell, sprite)
            dungeon.mark_floored(room)

class RoomView:
    def __init__(self, room):
        self.room = room
        self.texture_finder = TextureFinder()

    def generate_sprites(self, layout):
        for cell in self.room:
            sprite = self.make_sprite(layout, cell)
            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)

So the sprites are chosen all in that one go, and if there is a passageway at that time it’ll get selected, and otherwise it will not.

So when we’re not stepping … where do the passageways come from? At the end of the BuildStepper’s list, we have three calls, to finish, populate, and add_content. It is finish that tells the layout to finish the map, and that’s when we add paths to connect things up and then make passageways.

The BuildStepper has no knowledge of what is going on, nor should it have.

Could we arrange to have a temporary updateKSL kind of thing and then somehow back it out or eliminate it after each step?

Could ensure_connected and make_passages somehow unlock the KSL and do it over? Could we build some separate structure that the build stepper can display, without actually creating the real KSL?

update_KSL doesn’t know it is updating the real view’s sprite lists: we could certainly have it generate one just for the stepper. The details of the rooms would vary, since flooring is assigned randomly, but any permanent structures would persist, walls and such.

Let me see if I can sort out the rules here.

  1. We’re going to need a new feature in ensure_connected, such that when we add a path room to connect separate chunks, that room will only connect to the two rooms we intend to connect, not to any other rooms. Maybe we should address that earlier than I had intended.

  2. For any other wall between two rooms, we want to allocate one door, and never move it. Review how make_passages works.

  3. When we build all in one go, we only build the KSL at the very end.

  4. What if we add a SpriteList to the view, one that is temporary, and update that between each step, when stepping? In other words, make a local KeyedSpriteList and give it to the view to display?

Let’s try to spike that in.

The spike sort of works, but now (of course) the passages get reallocated each time around.

Since all this is just in aid of a debug view of things, maybe we’d do well to belay most of this and work on the actual problem. What is the actual problem, perhaps simplified in view of sad experience?

Dungeon Sections

We want to be able to define sections in the dungeon. We do that by declaring a subregion and populating it with a few rooms. We’ll need to ensure that the subregion is connected, and, ideally, we’ll set up final internal passages at that time, which should not be changed by anything later.

When two or more sections are complete, we want to connect them with a special kind of path room, which can only connect at its ends, to the specified rooms we selected for connection, not along its sides. (Probably the best way to accomplish this is not to use ensure_connected and make_passages for this.)

We’d like to be able to see all this happening in the build stepper, but the build stepper cannot be allowed to make any changes to the dungeon, though it could be allowed to create temporary sprite lists.

Design “Thinking”

If anything is to display in the stepper, we must allocate flooring. We could probably allocate some kind of temporary flooring, maybe just pink squares.

Every time a room is created, its borders, if any, with existing rooms, are computable. So what if, every time a room is created, we check its boundary and allocate a passageway right then. If a subsequent room touches an existing room, that boundary is new and the new room’s check could do the work.

We have some kind of passage structure now. What is it? DungeonLayoyut has a dictionary on pairs of cells indicating a passage. a passage between c1 and c2 will appear in the dictionary with key (c1,c2) and key (c2, c1).

Maybe an additional structure (r1,r2) and (r2,r1) saying the rooms have a passageway and we interrogate that when making new passages and don’t make any more or different ones?

The code makes me suspect that in stepping mode there may actually be more than one passage between rooms. I don’t see what would prevent that.

Monday

I had a seemingly good idea last night. Let’s see if I can remember it. I think it came in two parts.

The first idea I was playing with was to create a temporary sprite list that gets used during stepping. I’ve tried that once, and it sort of worked. But if I were to do that, how would I recognize the walls? Ah, and that’s what I forgot, using a different color tile for each room.

But then I thought no, the assignment of borders (and flooring) really does want to happen at two times: first, when a section is connected and logically complete. We can assign all its real flooring and internal doors at that point. We can do that for section. In principle, we could make different flooring decisions at that point, and even manually assign passages if desired.

Then “at the end”, we connect the sections. Think of a path between sections as a tunnel that gets bored between them. It may wander around, even come close to other rooms, but the tunneling machine only breaks through at the two ends. We will have selected the two cells that are intended to connect to the tunnel. If we arrange to have this kind of path only make connections at its ends, which it would do explicitly as part of the process, we could “force” those connections into an otherwise completed room.

I need to review what happens now.

The layout remembers floored_rooms and can return unfloored_rooms. I think we are moving rooms to floored too soon.

Let’s give it a new method all_rooms_floored and call it later.

Ah, good. Build steps are now like this:


def make_build_table(layout, dungeon):
    steps =  [
        # make_corner_rooms,
        make_diamond_in_round_room,
        finish,
        section_complete,

        bottom_left,
        make_a_diamond_room,
        make_a_diamond_room,
        make_a_diamond_room,
        finish,
        section_complete,

And finish does not lock the rooms, and section_complete does:

def finish(layout, dungeon):
    layout.finish_map()
    layout.clear_subregions()


def section_complete(layout, dungeon):
    layout.all_current_rooms_floored()

Let’s combine finish and section_complete into one method finish_section.

I have a scheme sort of working and committed with this message:

layout working in both stepping and non stepping mode, added section_complete. inter-section paths not yet making passages. We can add the passage. How can we change the tiles???

Currently each section is tiled each time a room is created but passages are only added at the end of the section, and at that point, the flooring is locked. The effect of that is that the internal passages never change, and the flooring is locked at that point. However, then when the final paths are created that join sections, they just abut the rooms they connect, and do not show an opening.

No, it’s even worse. The picture doesn’t change, but the passages get moved.

The issue seems to be resolvable by only making passages in unfloored rooms:

    def make_passages(self):
        for r1, r2 in itertools.combinations(self.unfloored_rooms, 2):
            passage = self.random_passage(r1, r2)
            if passage:
                print(f'{r1}->{r2}: {passage}')
            self.add_passage(passage)

There are perhaps other places that should now only consider unfloored rooms.

But I think the idea is fundamentally weak. We’re using the unfloored rooms idea to force an algorithm that tries to do all the rooms to just do the few we care about. What we need, it seems to me, is a scheme where we tell the flooring and passage makers and so on exactly which rooms we want dealt with, and what we want done.

This issue is exacerbated, and also made worse, by the stepper. We need the stepper to display flooring, or a simulacrum thereof, while each room is being built, each step of the way.

Perhaps … perhaps what we should do is create the flooring for a room right when it is built … but we don’t yet know where the passages will be.

Must reflect. This is most distressing.