Amid horrendous technical difficulties, I propose to build a strategy-based content item. In an astounding plot twist, we drop the Strategy idea!

My Black Magic Mouse battery is at 0%. In Apple’s great wisdom, the mouse’s charge port is on the bottom. You cannot use the mouse while it is charging. Thus, I am stuck with keyboard and track pad this morning, at least until the mouse takes on a credible charge. Despite this tragic sequence of events, I propose to build a working content item using the strategy pattern idea.

But first …,

I want to tell you about two ideas that I’ve had that seem somewhat good. I was thinking about rooms and doors and darkness. Two rooms are adjacent if they have at least one pair of tiles that are adjacent. We presently draw borders along all such adjacencies, and Dot can walk across any such border. In the fullness of time, we’ll make them unable to be walked through unless they are a door.

I was thinking that possibly we could display all such door cells with a small door texture, oriented vertically, and that it might look OK. Remains to be seen. But we presently display all the cells that are inside the window, so you can, in essence, see many rooms at once. In addition, if both door cells displayed a door, it would look weird.

So then I thought, what if we only displayed the cells in the room you’re in, leaving the rest of the dungeon dark, as it of course would be, since we can’t see through walls. So my plan is to do that. It will require some fairly substantial changes, I suspect. We’ll probably want to have a separate tile sprite list for each individual room, and that is really a view thing, so we’ll have to retain the RoomView instances, which we now create and drop as needed, and we’ll need to break out contents by room as well, since we don’t want contents in other rooms appearing out in space after the room itself is not displayed.

I think this will be a nice feature addition, and an interesting refactoring problem as well, so I am thinking we’ll schedule it soon.

But first, I want to get the Content sorted out. I think it’s a one- or two-session job, so we should be working on Project Darkness, a name I just made up, by Monday if not sooner. Of course, you probably know how good software estimates are, so … we’ll get to it soon. ish.

Strategy-based Content

I can see two ways of proceeding. We could extend the current new object in the test file to do the job, or we could refactor and refuctor the existing content items to get one working in the new style. The latter is tempting, but I think the former is likely to work better, even though we really can’t test it fully with pytest: it’ll be drawing and such.

I think the simplest content we have is DrawableContent, so we’ll try to build a strategy-based replacement. The existing class looks like this:

class DrawableContent(Content):
    error_texture = ':resources:images/items/star.png'
    def __init__(self, name, resource=None, scale=1):
        self.name = name
        self.scale = scale
        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, cell_size):
        texture = self.shape
        scale = scale_texture(texture, 0.5)
        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

I think the scale parameter is ignored, but should probably be used where that 0.5 is. We’ll see. Let’s look now at the test file.

class IgnorePlacedStrategy():
    def placed(self, *args, **kwargs):
        return 'ignored'

class SecretPlacedStrategy():
    def __init__(self):
        self.visible = False

    def placed(self, dungeon):
        self.visible = True
        def callback(event):
            dungeon.announce('A key has appeared!')
            self.visible = False
        dungeon.subscribe_once('button_pressed', callback)


class ContentItem:
    def __init__(self):
        self.placement_strategy = IgnorePlacedStrategy()

    def placed(self, dungeon):
        return self.placement_strategy.placed(dungeon)

I think the objective is to improve this ContentItem until it can be used to draw simple contents like the skulls and bones, which have no behavior. So for now, we’ll hard-code pass for the other ignored methods, just to save creating a bunch of Ignore … no. Let’s enhance the IgnoreStrategy to ignore all the methods we require, which are just draw, interact_with, and placed. We’ll rename that strategy and add to it:

class IgnoreStrategy():
    def draw(self, *args, **kwargs):
        return 'ignored'
    def interact_with_player(self, *args, **kwargs):
        return 'ignored'
    def placed(self, *args, **kwargs):
        return 'ignored'

Then our ContentItem needs to default its strategies to that. I am concerned about what we’ll do for the various inits that our objects need. Maybe they’ll be standardized. For now, we’ll edit it to be whatever this particular one needs.

class ContentItem:
    def __init__(self):
        self.drawing_strategy = IgnoreStrategy()
        self.interaction_strategy = IgnoreStrategy()
        self.placement_strategy = IgnoreStrategy()

    def draw(self, cx, cy, cell_size):
        return self.drawing_strategy.draw(cx, cy, cell_size)
    def interact_with_player(self, dungeon):
        return self.interaction_strategy.interact_with_player(dungeon)
    def placed(self, dungeon):
        return self.placement_strategy.placed(dungeon)

Now for this to work, we just need to plug in a drawing strategy. So we’ll create one:

class DrawingStrategy:
    def draw(self, texture, cx, cy, cell_size):
        scale = scale_texture(texture, 0.5)
        arcade.draw_texture_rect(
            texture,
            arcade.XYWH(cx, cy, texture.width, texture.height).scale(scale)
        )

Creating that told me that we’ll need to pass the texture from the ContentItem, since the Strategy will not know it. Unless we create it with the texture … let’s do that. Refactor / edit:

class DrawingStrategy:
    def __init__(self, resource):
        self.shape = arcade.load_texture(resource)

    def draw(self, cx, cy, cell_size):
        texture = self.shape
        scale = scale_texture(texture, 0.5)
        arcade.draw_texture_rect(
            texture,
            arcade.XYWH(cx, cy, texture.width, texture.height).scale(scale)
        )

We’ll leave out the error-checking for the resource. Get it right or expect an exception, for now.

Now let’s see if we can get this little arrangement into use.

In main, we create three instances of DrawableContent:

    item_4 = DrawableContent("skel", my_path + 'Skeleton1.png', scale=0.5)
    dungeon.place_content_at(Cell(30, 32), item_4)
    item_4 = DrawableContent("skel", my_path + 'Skeleton2.png', scale=0.5)
    dungeon.place_content_at(Cell(30, 26), item_4)
    item_4 = DrawableContent("chest", my_path + 'chest/1.png', scale=0.5)
    dungeon.place_content_at(Cell(34, 25), item_4)

Let’s just make one of these, the chest, be our new thing. Keep it simple until it works.

    item_4 = ContentItem()
    item_4.drawing_strategy = DrawingStrategy(my_path + 'chest/1.png')
    dungeon.place_content_at(Cell(34, 25), item_4)

Let’s fire it up and see what this baby can do.

map with circled chest image, woot!

Woot! The chest appears just as it should. Our strategy-based ContentItem works. I’ll replace the other two uses right now, in a spirit of enthusiasm and confidence.

    item_4 = ContentItem()
    item_4.drawing_strategy = DrawingStrategy(my_path + 'Skeleton1.png')
    dungeon.place_content_at(Cell(30, 32), item_4)
    item_4 = ContentItem()
    item_4.drawing_strategy = DrawingStrategy(my_path + 'Skeleton2.png')
    dungeon.place_content_at(Cell(30, 26), item_4)
    item_4 = ContentItem()
    item_4.drawing_strategy = DrawingStrategy(my_path + 'chest/1.png')

The picture remains perfect. Commit: using ContentItem+DrawingStrategy in place of DrawableContent.

Reflection

It works! Time to celebrate. But first, we should reflect on what we have and what we need and what we want.

We have working code for drawing. We need to extend it to deal with interaction and being placed, which I think will be tricky. Since I’m planning a break very soon, let’s quickly review what the other content items need to do, so as to prime my mind for off-line thinking.

The Button just has a publish command in interact_with_player, so it will be easy.

The SecretKey currently has a visible flag:

class SecretKey(ReceivableContent):
    def __init__(self, name='a secret key', resource=None):
        resource = '/Users/ron/Desktop/DungeonTiles/png/objects/Key (1).png'
        super().__init__(name, resource)
        self.visible = False

    def draw(self, cx, cy, cell_size):
        if self.visible:
            super().draw(cx, cy, cell_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):
            dungeon.announce('A key has appeared!')
            self.visible = True
        dungeon.subscribe_once('button_pressed', callback)

I’m struggling a bit here. If we build a SecretContentItem, subclassing our basic one … or perhaps holding on to one and deferring to it? Or …

I am somewhat distressed …

I’m starting to think that this idea is not as good as it seemed to be. The point of the idea was to remove duplication. Right now, in the old Content hierarchy, there are two methods that are often duplicated. One is draw, which I’m sure we can completely replace if we want to. The other is __init__, which is duplicated perfectly, I think, or could be … with one exception so far. The SecretKey needs a boolean member variable visible, which it uses to decide how to display itself and how to interact.

Well, wait …, maybe there is a SecretStrategy that contains all of draw (via a sub-strategy?), interact_with_player, and placed, together with a member variable in its init.

But that’s just the same as the current SecretKey, except more complicated due to the need to build strategy objects and wire them up.

Belay the Strategy Idea

That’s it. We’ll set the strategy idea aside for now. It doesn’t appear to be going to make things easier. Maybe when more objects are in place, we’ll come back to it, but I have my doubts.

It is conceivable that something a bit simpler, some kind of delegation that the object itself can choose, will make more sense.

Undo the changes to main, commit: remove use of strategy stuff, it’s not baked yet.

Summary

Should I feel badly because I built this thing only to discover that it isn’t as good as it seemed? Should I have been more risk-averse and not done it just because it might not work out? Should I have thought more deeply and somehow reasoned out that it wasn’t going to work?

The wonderful Diana Larsen said “Don’t should all over yourself”, and I try to heed that advice. I thought about options quite a bit, tried both Mixin and Strategy in a small way, thought Strategy seemed more tasty, built a fairly simple one, tried it in place, saw that it worked, and in thinking how to extend it, came to think that it gets too tricky to be entirely useful.

It took me two sessions to determine that I need a better idea, either “live with the duplication” or “find a better way to eliminate the duplication”, or even “inherit behavior, tell ‘em f___ ‘em.” Had I not taken those sessions to learn, I might have a good decision but I wouldn’t have the background to say why.

Would I like to be smarter? Sure, I suppose so. Given a choice, though, I might choose to be younger rather than smarter. As a great man once said, “it is what it is”. We’re here now, we know more, we’re not stuck in any sense.

And maybe we can even move on to those interesting ideas about doors and darkness sooner. That could be a good thing.

In the end, I’m happy that we made it work perfectly, and expect to be happy because we don’t have to struggle to make it work where it won’t work well. And we might even work out a better scheme sooner or later. We’re in the business of changing code, and sometimes that means changing it back the way it was.

See you next time, when we might add more code, changing the program in a fun way.