Let’s deal with some of the PubSub issues and needs.

Here’s our list of issues from last time.

  1. Provide for removing subscriptions even while publication is going on;
  2. Provide for adding new subscriptions as well. Again, deal with what happens when publication is running.
  3. Very likely we’ll just copy the list of subscribers, allowing the original to be updated harmlessly.
  4. Sort out the announcement logic, so that messages don’t all default to “You have received …”.
  5. Produce a reasonable hierarchy of ContentItems, including the ability to change their look as things happen, not just appear and disappear.

In addition, when Dot steps on the image of the flag she is given a flag, and if she were to step on the key before it is visible she’d be given it anyway. We’ll deal with that first.

I discover that you aren’t really given the flag. I had put an announcement in the interact method as a way to be sure it was happening. So that’s fixed.

class InvisibleKey(ContentItem):
    def __init__(self, name='vanishing key', resource=None):
        resource = ':resources:images/items/keyYellow.png'
        super().__init__(name, resource)
        self.visible = False

    def interact_with_player(self, dungeon):
        if self.visible:
            super().interact_with_player(dungeon)

That’s enough to keep it from being given when invisible.

Reflection

I think we need to sort out the You have received default message. Let’s see where that comes from.

class ContentItem:
    def interact_with_player(self, dungeon):
        dungeon.receive_item(self)
        dungeon.announce_received(self.name)

class Dungeon:
    def announce_received(self, item_name):
        self.announcements.append(item_name)

class DungeonView:
    def interact_with_player(self, dungeon):
        dungeon.receive_item(self)
        dungeon.announce_received(self.name)

    def on_update(self, delta_time: float):
        def receive(item_name):
            msg = f'    You have received {item_name}!'
            self.scroller.announce(msg)

        self.dungeon.announce_via(receive)
        self.scroller.update()

I’m wondering how messages ever come out that do not say You have received ....

Ah, this isn’t good. Some messages are sent directly to the Scroller, from DungeonView. But Dungeon has the announcements buffer, and it was probably my intention that all messages would go to Dungeon and then be read over to DungeonView on update.

Let’s give Dungeon announce, which appends a message, unmodified, to the announcements. (Possibly this would better be done with pub-sub but let’s not get that deep yet.)

class Dungeon:
    def announce(self, message):
        self.announcements.append(message)

And I remove announce_received and update this method to do the ‘You have received’ bit:

    def interact_with_player(self, dungeon):
        dungeon.receive_item(self)
        dungeon.announce(f'You have received {self.name}!')

This seems nearly good. But we still have people talking directly to the Scroller. Find and fix those.

I change Scroller.announce to append:

class Scroller:
    def append(self, message: str) -> None:
        batch = BorderedText(message, font_size=24, base_x=self.base_x)
        self.buffer.append(batch)

Find the folx doing that and change them thus:

class DungeonView:
    def make_initial_announcement(self):
        for message in [
            'Welcome to the Friendly Dungeon of Doom',
            'Here you will encounter many new friends,',
            'some of whom will not attack you.',
            'Some of you may die, but we are willing to make that sacrifice.',
            'Good luck! You will need it.'
        ]:
            self.dungeon.announce(message)

And similarly throughout. There are two tests breaking. They were still using scroller announce, since I changed that name directly, did not use the rename refactoring because there were too many different versions of announce. We are green. Commit.

Now I think we should fix up the hierarchy in ContentItem. We currently have three different kinds:

ContentItem
Root of the hierarchy, and can be given to the player and added to inventory. (There is currently no separate Player object, probably should be.)
InvisibleKey
Listens for a button and only draws itself or gives itself when it is visible. Defaults to invisible.
Button
Displays like a flag, when stepped on publishes event ‘button_pressed’.

I think we want something like this:

Content (abstract)
--EventContent
--ReceivableContent
----HiddenReceivableContent

I m in no sense locked in on that and it might be too much for as far along as we are. But let’s do at least the abstract base class and see what we get.

class Content(ABC):
    @abstractmethod
    def draw(self, cx, cy, size):
        pass
    @abstractmethod
    def interact_with_player(self, dungeon):
        pass
    @abstractmethod
    def placed(self, dungeon):
        pass

That compiles and the tests all run. Commit: building Content hierarchy.

What I think I’ll do is make pure concrete classes under this and then see if there are some intervening classes with convenient methods that are in common. I will permit superclasses to contain behavior, unlike some of my colleagues.

Let’s see, what’s next? Darn it, once you set out to do this you’re in for a bunch of work. Let’s do a Drawable superclass.

I go hog wild and we wind up with this hierarchy:

Content
--DrawableContent
----Button
----ReceivableContent
------SecretKey

And this code:

class Content(ABC):
    @abstractmethod
    def draw(self, cx, cy, size):
        pass
    @abstractmethod
    def interact_with_player(self, dungeon):
        pass
    @abstractmethod
    def placed(self, dungeon):
        pass

class DrawableContent(Content):
    error_texture = ':resources:images/items/star.png'
    def __init__(self, name, resource=None):
        self.name = name
        try:
            self.shape = arcade.load_texture(resource)
        except:
            self.name = 'invalid resource: ' + name
            self.shape = arcade.load_texture(self.error_texture)

    def draw(self, cx, cy, size):
        texture = self.shape
        scale = size/128
        arcade.draw_texture_rect(
            texture,
            arcade.XYWH(cx, cy, texture.width, texture.height).scale(scale)
        )

    def interact_with_player(self, dungeon):
        pass
    def placed(self, dungeon):
        pass

class Button(DrawableContent):
    def __init__(self, name, resource=None):
        resource = ':resources:images/items/flagGreen1.png'
        super().__init__('button', resource)

    def interact_with_player(self, dungeon):
        dungeon.publish('button_pressed')

    def placed(self, dungeon):
        pass

class ReceivableContent(DrawableContent):
    def interact_with_player(self, dungeon):
        dungeon.receive_item(self)
        dungeon.announce(f'You have received {self.name}!')

    def placed(self, dungeon):
        pass

class SecretKey(ReceivableContent):
    def __init__(self, name='a secret key', resource=None):
        resource = ':resources:images/items/keyYellow.png'
        super().__init__(name, resource)
        self.visible = False

    def draw(self, cx, cy, size):
        if self.visible:
            super().draw(cx, cy, size)

    def interact_with_player(self, dungeon):
        if self.visible:
            super().interact_with_player(dungeon)

    def placed(self, dungeon):
        self.visible = False
        def callback(event):
            self.visible = True
        dungeon.subscribe('button_pressed', callback)

Way more work than I planned for an afternoon’s lark. Let’s sum up.

Summary

We fixed up an excess debug announcement, and changed the SecretKey not to give itself when it is invisible. Then moved on to sorting announcements so that all of them go to Dungeon and the View fetches them and displays them on_update.

Then, in a fit of madness, we created a little class hierarchy with an abstract base class at the top, under which our concrete content types currently reside, in flavors of Content, DrawableContent, Button, ReceivableContent, and SecretKey. These are almost certainly wrong and we may wind up changing things about or even possibly doing some kind of mixin scheme. It would be entirely reasonable to create the one Content base class and derive everything directly, accepting duplication of some method code.

We’ll see how it goes. We’re not locked in to anything. For now, things are progressing toward something that seems more like generally useful capability.

Plenty to do. Maybe next time we’ll improve the graphics. To do that I’ll probably have to round some up somewhere.

See you next time!