Hello, loves!

This exercise has me hooked. I’m still engaged with getting DungeonView pared down until it looks … much better. I’d say perfect, but there is no perfect: only better.

I had a few minutes between taking out the trash and the pizza being ready, so I did a few little changes last evening.

Viz:

Renamed the ContentSpriteMaker née ContentView file to match the new class name.

In-lined and removed the self.sprite_list member.

class DungeonView(arcade.View):
    def __init__(self, dungeon):
        if arcade.window_commands._window:
            super().__init__()
            self.cameras = Cameras(self, dungeon.max_x, dungeon.max_y, zoom=4)
        self.dungeon = dungeon
        self.pub_sub = dungeon.pub_sub
        self.subscribe(dungeon, self.pub_sub)
        self.setup_assets()
        self.keyed_sprites = KeyedSpriteList(arcade.SpriteList())
        self.content_sprites_by_cell: dict[Cell, list[Sprite]] = defaultdict(list)
        self.keys = KeyPress(self, self.dungeon, self.pub_sub)

Removed the unused sprite_list parameter from a few methods:

    def setup(self):
        self.create_room_sprites()
        self.create_content_lists()

    def create_content_lists(self):
        for cell, content in self.dungeon.layout.contents.items():
            for item in content:
                self.make_view_and_sprite(cell, item)

    def make_new_content(self, cell, item):
        self.make_view_and_sprite(cell, item)

    def make_view_and_sprite(self, cell, item):
        sprite = ContentSpriteMaker(item.resources, item.scale).sprite
        sprite.position = cell.center_position(cell_size)
        self.content_sprites_by_cell[cell].append(sprite)
        self.keyed_sprites.add(item, sprite)
        if self.keyed_sprites[cell].visible:
            sprite.visible = True

The first of those above used to pass self.sprite_list and the other two used to receive it. Now they are just a bit simpler.

We’re at 134 lines in DungeonView now.

Tentative Plan

We’ll scan the code before deciding, but other than a few small improvements, which we’ll make if we notice them, I think the most improvement might come from offloading the initialization code for DungeonView, to some kind of DungeonViewMakerUpper class, leaving only the game run-time code in the actual class.

I”m curious how many methods we have, and given a list, we can sort them into groups. We’re down to about 20. I think there used to be 34 or something like that. Give me a moment here, I’ll sort them into two or more groups:

<two groups removed because the change below changes the words. Vide infra.>

There is run-time code inside the two subscribe_to methods executed by the content_sprite_do method. make_view_and_sprite is used both in prep and run time, since new content can be created during the run.

I noticed that removal of references to sprite_list leaves this arrangement:

    def create_content_lists(self):
        for cell, content in self.dungeon.layout.contents.items():
            for item in content:
                self.make_view_and_sprite(cell, item)

    def make_new_content(self, cell, item):
        self.make_view_and_sprite(cell, item)

    def make_view_and_sprite(self, cell, item):
        sprite = ContentSpriteMaker(item.resources, item.scale).sprite
        sprite.position = cell.center_position(cell_size)
        self.content_sprites_by_cell[cell].append(sprite)
        self.keyed_sprites.add(item, sprite)
        if self.keyed_sprites[cell].visible:
            sprite.visible = True

There are two issues here, at least two. First, the make_new_content method adds nothing but a name, which is in fact called externally: it’s called back from Dungeon during the code that makes the Path of Skulls to show where the red key is. (We should probably be doing that with a publish anyway.) Second, we don’t make any views here. We make and record sprites. Let’s change Dungeon to call the other method:

class Dungeon:
    def show_path_to(self, item_name, dungeon_view):
        path = self._find_path_to(item_name)
        time_out =  5 # seconds
        for time, cell in enumerate(path, start=time_out):
            item = ContentFactory().temp(name="skel",
                                         time=time,
                                         resource='Skeleton1.png',
                                         scale=0.5)
            cell.add_content(item)
            item.run(self.pub_sub)
            # TODO maybe use publish
            dungeon_view.make_view_and_sprite(cell, item)

Commit that. Now remove the unused method. Commit. Rename the remaining one.

class DungeonView:
    def create_content_lists(self):
        for cell, content in self.dungeon.layout.contents.items():
            for item in content:
                self.allocate_content_sprite(cell, item)

    def allocate_content_sprite(self, cell, item):
        sprite = ContentSpriteMaker(item.resources, item.scale).sprite
        sprite.position = cell.center_position(cell_size)
        self.content_sprites_by_cell[cell].append(sprite)
        self.keyed_sprites.add(item, sprite)
        if self.keyed_sprites[cell].visible:
            sprite.visible = True

Now I get to make my two lists again. 19 interesting methods now:

Run-Time

allocate_content_sprite
content_sprite_do
draw_adventurer
draw_flood
illuminate_around_dot
illuminate_cell
move_player
on_draw
on_key_press
on_key_release
on_update

Prep-Time

create_content_lists
create_room_sprites
run
setup
setup_assets
subscribe
subscribe_to_remove_content
subscribe_to_state_number

I think for the prep-time ones, I’m interested in which methods call which others. Let me gin that up.

__init__
    setup_assets
    subscribe
        subscribe_to_remove_content
        subscribe_to_state_number
run
    setup
        create_content_lists
            (allocate_content_sprite)
        create_room_sprites
    (self.dungeon.run())
    (illuminate_around_dot)
    (self.window.show_view)
    (arcade.run)

It looks to me as if we should be able to call setup from __init__. If we can, that would simplify run and take away any aspect of Prep that it has. Two tests fail when we do that. The game runs perfectly. What’s up with those tests? They are complaining about not getting borders and such.

In main we fine this code:

    layout.ensure_connected()
    layout.make_passages()
    layout.make_borders()
    dungeon = Dungeon(layout)
    dungeon.populate()

Main has been giving the layout and dungeon time to wrap up their affairs and populate. Reset that change: we need to think.

Thinking … not that I wasn’t thinking before …

It’s pretty clear that those three calls to layout should be in a layout method and that it should be called when one is finished building the layout, or perhaps called by Dungeon when it is given the layout.

As things stand now, the main, after creating the Dungeon as shown above, installs the contents. So we can’t move the setup to __init__ if we’re going to leave Dungeon open to having content added to it. Content is in the layout, by the way, not the dungeon.

So, let’s at least encapsulate those three calls to layout into one method. Those three steps are what we want done once all the specified rooms are defined. Those three calls make sure that the rooms all connect, identify where to put passages between them, and set up what we need to know about borders to do the floor layout.

What is that operation called? Let’s call it finish_map:

class DungeonLayout:
    def finish_map(self):
        self.ensure_connected()
        self.make_passages()
        self.make_borders()

Still thinking: Should we call finish_map from Dungeon upon creation with a layout? Should we perhaps have a map-building thing that creates an immutable layout for use by Dungeon?

I think we’ll call finish_map from Dungeon.

class Dungeon:
class Dungeon:
    def __init__(self, layout):
        layout.finish_map()
        self.layout = layout
        self.player_cell = None
        self.player_inventory = []
        self.announcements = []
        self.pub_sub = PubSub()
        def callback(*, pub_sub, message):
            self._announce(message)
        self.pub_sub.subscribe('announce','', callback)
        self.flood_list = SpriteList()

Three tests break. The issue is that the tests are often set u so that there are no connections between rooms, to ensure that the right things happen. So they are not happy when the dungeon goes ahead and makes new paths and such.

We could have a flag in the layout that tells it that it is finished and should not finish again. Then our tests could fudge around with that and find out what they want to know. Seems hackish though.

Let’s turn our attention back to DungeonView and see what we can do without changing too much of what’s underneath.

Stop

I suddenly realize that it is time to stop. I’ve sort of lost headway. What we need next—or what I can see of it—is more than I have time and energy for. I do make one tidying change: I arrange the methods in the source file into sections, reflecting the calling order shown above. I am left with these methods, which are called, I believe, only during actual game execution:

on_draw
    draw_adventurer
    draw_flood
on_update
on_key_press
on_key_release
move_player
illuminate_around_dot
    illuminate_cell

Those methods need access to: dungeon,keys, keyed_sprites, content_sprite_by_cell, cameras. I think that’s all.

Therefore, we could, after all the setup stuff, create an actual View that contained just the methods above and those five members, and we’d have the division we want.

That seems feasible. But not today, Satan. I’m tired and prone to error. Time for chai, maybe a granola bar.

See you next time!