A few improvements, a bit of design thinking, and a plan takes shape. A good morning.

Hello, loves. What shall we do today? We’ll certainly review what we did yesterday afternoon, the little table-driven addition to Animated, the class that supports our spikes. If nothing else, we’ll probably give that class a noun for a name. Seems only fair. Let’s review my reflections from yesterday, and a few of my notes on cards that might be interesting:

  • Deal with possible missing values in the control dictionary.
  • Provide for other control mappings without requiring a new constructor.
  • Should Lever refuse to allow entry, merely switching when Dot tries to step on it? What are the implications for placement?
  • Tests or something to keep PubSub forwarder in Dungeon in sync with PubSub. Abstract Class?
  • Appeared message starts with lower case. Capitalize?
  • Make PubSub weak. Problem: I can’t figure out how. Yet.

There are more, mostly just small things. Some where I don’t understand my notes. Some just seemed so boring I didn’t even copy them into the article.

Let’s have a look at the announcer and see if we can make it capitalize the first letter of every message.

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

Right. The done thing is to slice the message because, in its wisdom capitalize also lower-cases the rest of the string. We have no useful test for announcements. Make a note of that: refactor announcements to its own class with tests.

Anyway it is simple and easy to check.

    def announce(self, message):
        capped = message[:1].upper() + message[1:]
        self.announcements.append(capped)

Checked in game. Works. This idiom is so well-known that PyCharm knew what I wanted even with its “AI” turned off. Which it is. Commit: capitalize announcements.

Reviewing the code, since it was right there, I’m reminded that Dungeon just accumulates a list of announcements and DungeonView fetches them like this:

class DungeonView:
    def on_update(self, delta_time: float):
        def receive_announcement(msg):
            self.scroller.append(msg)

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

class Dungeon:
    def announce_via(self, callback):
        for announcement in self.announcements:
            callback(announcement)
        self.announcements = []

So there’s not much there to test. That’s not a very good excuse for there being essentially no tests of this material. In fact, I think it’s evidence of a problem that is becoming endemic in this program:

Certain key classes, such as Dungeon and DungeonView, require extensive initialization in order to operate at all, and we cannot easily test them without all of Arcade running and a window opening. There are a couple of hacks for testing but those classes are central to operation and they have become accumulators of function, resulting in important capabilities that are difficult to test. That results in less test-driving. That results in slower progress, more debugging by running the game or printing things, and allows more defects to remain in the code.

In short, it’s getting messy, and has been for some time.

My shtick

… as all here know, is the claim that we can always improve the code where it needs it, in small steps that can each be committed, without ever needing to resort to long dry feature-desert treks. I demonstrate that by getting into trouble—which seems to come naturally to me—and then getting out of it in a few two-hour sessions with lots of commits.

Now I generally suggest that in a real product environment, if some area in the code has gone off but we are not working with it, we should not proactively refactor it. I suggest that because my biggest fear in development is that we’ll be doing good work, but not delivering enough visible value, and the Powers That Be will decide we’re not doing the right thing and cancel our product. If I were somehow condemned to product development again, I would try to avoid such cancellations by delivering a steady stream of features, at as close to a constant pace as we could manage.

And the way I work here is my best attempt to show how we’d do that.

So it’s getting messy, as things do, and when we find ourselves working in messy areas, we’ll bear down a bit on cleaning them up.

However

… because we’re here in a simulated environment, my living room — of course my computer is set up in my living room. Isn’t yours? — we have the luxury of pretending to have something to do in a messy class, or devising something we have to do, and then proceeding to show how we’d do it. I think such simulations are entirely valid and germane to real development. So, we might, from time to time, just improve something.

And, I guess, the fact is that any development project can probably afford a couple of hours per person per week to improve things. The trick is to keep all the improvements in the production version.

So, you do you, and I’ll do me. And I’ll keep improving those classes, with tests, on my list.

What shall we do now, though?

Let’s review the Animated class and get a sense of what it really is and how it fits into the scheme of things. We will surely find some things to improve. Here’s the whole thing:

class Animated(Content):
    @classmethod
    def spikes(cls, name):
        resource1 = '/Users/ron/Desktop/DungeonTiles/png/objects/trap/1.png'
        resource2 = '/Users/ron/Desktop/DungeonTiles/png/objects/trap/2.png'
        resources = [resource1, resource2]
        cases = {
            0: (True, 0),
            1: (False, 1),
            2: (False, 0),
            3: (True, 0),
        }
        return cls(name, resources, cases)

    def __init__(self, name, resources, cases):
        self.cases = cases
        self.resources = resources
        self.name = name
        self.cycling = True
        self.time = 0
        self.scale = 0.75
        self.state = 0

    def set_state(self, cycling, state, dungeon):
        self.cycling = cycling
        self.state = state
        dungeon.publish('state_number', 'view', self, self.state)

    def interact_with_player(self, dungeon):
        pass

    def run(self, dungeon):
        self.cycling = True
        self.state = 0
        self.time = 0
        def callback(delta_time):
            if not self.cycling: return
            self.time += delta_time
            if self.time >= 1:
                self.time = 0
                self.set_state(True, (self.state + 1) % len(self.resources), dungeon)

        dungeon.subscribe('on_update', 'view', callback)
        def control_changed(control, state_number):
            cycling, state = self.cases[state_number]
            self.set_state(cycling, state, dungeon)

        dungeon.subscribe('state_number', self.name, control_changed)

There are things to be concerned about here. The easy one will be the run method, which is too long and does at least two, arguably three things. It initializes the cycling logic, it sets up a callback for updates, and it sets up a callback for hearing state number from its controllers.

I do not like PyCharm’s inclination to put a blank line after the callback function, because it tends to make blocks of code that are confusing, because they don’t belong together. Might fix that. Anyway we’ll break out three methods, I think, like this:

    def run(self, dungeon):
        self.initialize_cycling()
        self.subscribe_to_on_update(dungeon)
        self.subscribe_to_controls(dungeon)

    def initialize_cycling(self):
        self.cycling = True
        self.state = 0
        self.time = 0

    def subscribe_to_on_update(self, dungeon):
        def callback(delta_time):
            if not self.cycling: return
            self.time += delta_time
            if self.time >= 1:
                self.time = 0
                self.set_state(True, (self.state + 1) % len(self.resources), dungeon)
        dungeon.subscribe('on_update', 'view', callback)

    def subscribe_to_controls(self, dungeon):
        def control_changed(control, state_number):
            cycling, state = self.cases[state_number]
            self.set_state(cycling, state, dungeon)
        dungeon.subscribe('state_number', self.name, control_changed)

Green. Commit: tidying.

I like the callback named control_changed better than callback. Rename that one … how about something about cycling? update_cycling? We’ll try that.

    def subscribe_to_on_update(self, dungeon):
        def update_cycling(delta_time):
            if not self.cycling: return
            self.time += delta_time
            if self.time >= 1:
                self.time = 0
                self.set_state(True, (self.state + 1) % len(self.resources), dungeon)
        dungeon.subscribe('on_update', 'view', update_cycling)

Oops missed a commit. Commit now.

I just noticed that initialize_cycling doesn’t use set_state, which makes sure the view stays in sync. Let’s do that:

    def initialize_cycling(self, dungeon):
        self.set_state(cycling=True, state=0, dungeon=dungeon)
        self.time = 0

Python allows us to name parameters as I did there, and even checks them if we do. I had to add the parameter dungeon to the method, so we could use it. Commit.

Let’s do that with the other calls. Here we are after that:

    def __init__(self, name, resources, cases):
        self.cases = cases
        self.resources = resources
        self.name = name
        self.cycling = True
        self.time = 0
        self.scale = 0.75
        self.state = 0

    def set_state(self, cycling, state, dungeon):
        self.cycling = cycling
        self.state = state
        dungeon.publish('state_number', 'view', self, self.state)

    def interact_with_player(self, dungeon):
        pass

    def run(self, dungeon):
        self.initialize_cycling(dungeon)
        self.subscribe_to_on_update(dungeon)
        self.subscribe_to_controls(dungeon)

    def initialize_cycling(self, dungeon):
        self.set_state(cycling=True, state=0, dungeon=dungeon)
        self.time = 0

    def subscribe_to_on_update(self, dungeon):
        def update_cycling(delta_time):
            if not self.cycling: return
            self.time += delta_time
            if self.time >= 1:
                self.time = 0
                self.set_state(cycling=True, state=(self.state + 1) % len(self.resources), dungeon=dungeon)
        dungeon.subscribe('on_update', 'view', update_cycling)

    def subscribe_to_controls(self, dungeon):
        def control_changed(control, state_number):
            cycling, state = self.cases[state_number]
            self.set_state(cycling=cycling, state=state, dungeon=dungeon)
        dungeon.subscribe('state_number', self.name, control_changed)

A bit verbose but I think I like it. I am tempted to move toward keyword arguments in general, but so far, only tempted.

Commit.

While we are looking right at it, let’s bullet-proof that fetch from self.cases. We’ll ask forgiveness rather than use get.

    def subscribe_to_controls(self, dungeon):
        def control_changed(control, state_number):
            try:
                cycling, state = self.cases[state_number]
            except KeyError:
                return
            self.set_state(cycling=cycling, state=state, dungeon=dungeon)
        dungeon.subscribe('state_number', self.name, control_changed)

Commit: bullet-proof fetching cycling and state from cases.

Reflection

As I look at Animated and the fact that it currently only supports spikes, I think it could use a better name than ‘Animated’, which might be useful elsewhere. The current situation is a trap or obstacle, depending on whether we allow Dot to come to harm. Maybe we could let her get tired or something. There could be other such obstacles. Imagine a patch of water that blocks the way and that Dot cannot cross because she’d get her boots wet. A control of some kind might turn the water into tiles, or cast a bridge across, or turn the water to ice.

What is this object? It currently only changes its state in response to a control. Its state changes are subscribed to by DungeonView, and sent to a corresponding ContentView, which, given state N, sets the view to its Nth sprite texture.

It does not yet have any behavior on interaction. Presumably it would cause damage or refuse entry or some such thing yet to be determined.

I keep coming back to this vague idea:

Our content items, it seems to me, can have various behavior for displaying, presumably with a texture per some kind of display state. They can have various behavior for interaction, most of which is not yet defined. Presently these differences are embedded in classes: Content(AbstractBaseClass), DrawableContent, ReceivableContent, DungeonControl, Animated, and AppearingContent.

DrawableContent has no behavior at all, and just one texture. That seems like a sort of degenerate case of Animated. We could surely combine those two classes.

But it really seems to me that what would be better would be to have some kind of single object into which we would plug behavior as needed to make it do what we want, instead of ginning up a class for each combination, or filling classes with conditional code.

We’ve done a bit of that in this series, and then mostly found simpler ways to do things, with a few exceptions that might still be in there somewhere.

What I do not yet see is how to consolidate these classes, using, among other notions, Method Object, Strategy, pluggable behavior.

Probably the thing is to try it, either enhancing a current class, perhaps Receivable or Animated, with a new feature, and then refactor to a better solution, or perhaps we’d just try combining two classes, like Drawable and Animated …

It occurs to me that there is something in between Drawable and Animated. Animated includes the cycling. There could be a ControlledContent that has multiple display states but no ongoing animation. The water hazards I described would be like that. So there is a sort of conceptual thread from Drawable to ControlledDrawable to AnimatedDrawable … interesting …

There is no doubt that Content is one of our messy areas, with all those classes. And there’s no doubt that we have need of features in the content area.

So. Good. We’ll create some Content stories, implement them, and make things better as we go. Unless we fail. Sometimes we fail. Then we try again, and fail better.

Summary

A few improvements, a bit of design thinking, and a plan takes shape. A good morning.

See you next time!